procfs_core/process/
mount.rs

1use bitflags::bitflags;
2
3use crate::{from_iter, ProcResult};
4
5use std::collections::HashMap;
6use std::io::{BufRead, Lines};
7use std::path::PathBuf;
8use std::time::Duration;
9
10#[cfg(feature = "serde1")]
11use serde::{Deserialize, Serialize};
12
13bitflags! {
14    #[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
15    #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
16    pub struct NFSServerCaps: u32 {
17
18        const NFS_CAP_READDIRPLUS = 1;
19        const NFS_CAP_HARDLINKS = (1 << 1);
20        const NFS_CAP_SYMLINKS = (1 << 2);
21        const NFS_CAP_ACLS = (1 << 3);
22        const NFS_CAP_ATOMIC_OPEN = (1 << 4);
23        const NFS_CAP_LGOPEN = (1 << 5);
24        const NFS_CAP_FILEID = (1 << 6);
25        const NFS_CAP_MODE = (1 << 7);
26        const NFS_CAP_NLINK = (1 << 8);
27        const NFS_CAP_OWNER = (1 << 9);
28        const NFS_CAP_OWNER_GROUP = (1 << 10);
29        const NFS_CAP_ATIME = (1 << 11);
30        const NFS_CAP_CTIME = (1 << 12);
31        const NFS_CAP_MTIME = (1 << 13);
32        const NFS_CAP_POSIX_LOCK = (1 << 14);
33        const NFS_CAP_UIDGID_NOMAP = (1 << 15);
34        const NFS_CAP_STATEID_NFSV41 = (1 << 16);
35        const NFS_CAP_ATOMIC_OPEN_V1 = (1 << 17);
36        const NFS_CAP_SECURITY_LABEL = (1 << 18);
37        const NFS_CAP_SEEK = (1 << 19);
38        const NFS_CAP_ALLOCATE = (1 << 20);
39        const NFS_CAP_DEALLOCATE = (1 << 21);
40        const NFS_CAP_LAYOUTSTATS = (1 << 22);
41        const NFS_CAP_CLONE = (1 << 23);
42        const NFS_CAP_COPY = (1 << 24);
43        const NFS_CAP_OFFLOAD_CANCEL = (1 << 25);
44    }
45}
46
47/// Information about a all mounts in a process's mount namespace.
48///
49/// This data is taken from the `/proc/[pid]/mountinfo` file.
50pub struct MountInfos(pub Vec<MountInfo>);
51
52impl MountInfos {
53    /// Returns an borrowed iterator.
54    pub fn iter(&self) -> std::slice::Iter<'_, MountInfo> {
55        self.into_iter()
56    }
57}
58
59impl crate::FromBufRead for MountInfos {
60    fn from_buf_read<R: BufRead>(r: R) -> ProcResult<Self> {
61        let lines = r.lines();
62        let mut vec = Vec::new();
63        for line in lines {
64            vec.push(MountInfo::from_line(&line?)?);
65        }
66
67        Ok(MountInfos(vec))
68    }
69}
70
71impl IntoIterator for MountInfos {
72    type IntoIter = std::vec::IntoIter<MountInfo>;
73    type Item = MountInfo;
74
75    fn into_iter(self) -> Self::IntoIter {
76        self.0.into_iter()
77    }
78}
79
80impl<'a> IntoIterator for &'a MountInfos {
81    type IntoIter = std::slice::Iter<'a, MountInfo>;
82    type Item = &'a MountInfo;
83
84    fn into_iter(self) -> Self::IntoIter {
85        self.0.iter()
86    }
87}
88
89/// Information about a specific mount in a process's mount namespace.
90///
91/// This data is taken from the `/proc/[pid]/mountinfo` file.
92///
93/// For an example, see the
94/// [mountinfo.rs](https://github.com/eminence/procfs/tree/master/procfs/examples) example in the
95/// source repo.
96#[derive(Debug, Clone)]
97#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
98pub struct MountInfo {
99    /// Mount ID.  A unique ID for the mount (but may be reused after `unmount`)
100    pub mnt_id: i32,
101    /// Parent mount ID.  The ID of the parent mount (or of self for the root of the mount
102    /// namespace's mount tree).
103    ///
104    /// If the parent mount point lies outside the process's root directory, the ID shown here
105    /// won't have a corresponding record in mountinfo whose mount ID matches this parent mount
106    /// ID (because mount points that lie outside the process's root directory are not shown in
107    /// mountinfo).  As a special case of this point, the process's root mount point may have a
108    /// parent mount (for the initramfs filesystem) that lies outside the process's root
109    /// directory, and an entry for  that mount point will not appear in mountinfo.
110    pub pid: i32,
111    /// The value of `st_dev` for files on this filesystem
112    pub majmin: String,
113    /// The pathname of the directory in the filesystem which forms the root of this mount.
114    pub root: String,
115    /// The pathname of the mount point relative to the process's root directory.
116    pub mount_point: PathBuf,
117    /// Per-mount options
118    pub mount_options: HashMap<String, Option<String>>,
119    /// Optional fields
120    pub opt_fields: Vec<MountOptFields>,
121    /// Filesystem type
122    pub fs_type: String,
123    /// Mount source
124    pub mount_source: Option<String>,
125    /// Per-superblock options.
126    pub super_options: HashMap<String, Option<String>>,
127}
128
129impl MountInfo {
130    pub fn from_line(line: &str) -> ProcResult<MountInfo> {
131        let mut split = line.split_whitespace();
132
133        let mnt_id = expect!(from_iter(&mut split));
134        let pid = expect!(from_iter(&mut split));
135        let majmin: String = expect!(from_iter(&mut split));
136        let root = expect!(from_iter(&mut split));
137        let mount_point = expect!(from_iter(&mut split));
138        let mount_options = {
139            let mut map = HashMap::new();
140            let all_opts = expect!(split.next());
141            for opt in all_opts.split(',') {
142                let mut s = opt.splitn(2, '=');
143                let opt_name = expect!(s.next());
144                map.insert(opt_name.to_owned(), s.next().map(|s| s.to_owned()));
145            }
146            map
147        };
148
149        let mut opt_fields = Vec::new();
150        loop {
151            let f = expect!(split.next());
152            if f == "-" {
153                break;
154            }
155            let mut s = f.split(':');
156            let opt = match expect!(s.next()) {
157                "shared" => {
158                    let val = expect!(from_iter(&mut s));
159                    MountOptFields::Shared(val)
160                }
161                "master" => {
162                    let val = expect!(from_iter(&mut s));
163                    MountOptFields::Master(val)
164                }
165                "propagate_from" => {
166                    let val = expect!(from_iter(&mut s));
167                    MountOptFields::PropagateFrom(val)
168                }
169                "unbindable" => MountOptFields::Unbindable,
170                _ => continue,
171            };
172            opt_fields.push(opt);
173        }
174        let fs_type: String = expect!(from_iter(&mut split));
175        let mount_source = match expect!(split.next()) {
176            "none" => None,
177            x => Some(x.to_owned()),
178        };
179        let super_options = {
180            let mut map = HashMap::new();
181            let all_opts = expect!(split.next());
182            for opt in all_opts.split(',') {
183                let mut s = opt.splitn(2, '=');
184                let opt_name = expect!(s.next());
185                map.insert(opt_name.to_owned(), s.next().map(|s| s.to_owned()));
186            }
187            map
188        };
189
190        Ok(MountInfo {
191            mnt_id,
192            pid,
193            majmin,
194            root,
195            mount_point,
196            mount_options,
197            opt_fields,
198            fs_type,
199            mount_source,
200            super_options,
201        })
202    }
203}
204
205/// Optional fields used in [MountInfo]
206#[derive(Debug, Clone)]
207#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
208pub enum MountOptFields {
209    /// This mount point is shared in peer group.  Each peer group has a unique ID that is
210    /// automatically generated by the kernel, and all mount points in the same peer group will
211    /// show the same ID
212    Shared(u32),
213    /// THis mount is a slave to the specified shared peer group.
214    Master(u32),
215    /// This mount is a slave and receives propagation from the shared peer group
216    PropagateFrom(u32),
217    /// This is an unbindable mount
218    Unbindable,
219}
220
221/// A single entry in [MountStats].
222#[derive(Debug, Clone)]
223#[cfg_attr(test, derive(PartialEq))]
224#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
225pub struct MountStat {
226    /// The name of the mounted device
227    pub device: Option<String>,
228    /// The mountpoint within the filesystem tree
229    pub mount_point: PathBuf,
230    /// The filesystem type
231    pub fs: String,
232    /// If the mount is NFS, this will contain various NFS statistics
233    pub statistics: Option<MountNFSStatistics>,
234}
235
236/// Mount information from `/proc/<pid>/mountstats`.
237#[derive(Debug, Clone)]
238#[cfg_attr(test, derive(PartialEq))]
239#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
240pub struct MountStats(pub Vec<MountStat>);
241
242impl crate::FromBufRead for MountStats {
243    /// This should correspond to data in `/proc/<pid>/mountstats`.
244    fn from_buf_read<R: BufRead>(r: R) -> ProcResult<Self> {
245        let mut v = Vec::new();
246        let mut lines = r.lines();
247        while let Some(Ok(line)) = lines.next() {
248            if line.starts_with("device ") {
249                // line will be of the format:
250                // device proc mounted on /proc with fstype proc
251                let mut s = line.split_whitespace();
252
253                let device = Some(expect!(s.nth(1)).to_owned());
254                let mount_point = PathBuf::from(expect!(s.nth(2)));
255                let fs = expect!(s.nth(2)).to_owned();
256                let statistics = match s.next() {
257                    Some(stats) if stats.starts_with("statvers=") => {
258                        Some(MountNFSStatistics::from_lines(&mut lines, &stats[9..])?)
259                    }
260                    _ => None,
261                };
262
263                v.push(MountStat {
264                    device,
265                    mount_point,
266                    fs,
267                    statistics,
268                });
269            }
270        }
271
272        Ok(MountStats(v))
273    }
274}
275
276impl IntoIterator for MountStats {
277    type IntoIter = std::vec::IntoIter<MountStat>;
278    type Item = MountStat;
279
280    fn into_iter(self) -> Self::IntoIter {
281        self.0.into_iter()
282    }
283}
284
285/// Only NFS mounts provide additional statistics in `MountStat` entries.
286//
287// Thank you to Chris Siebenmann for their helpful work in documenting these structures:
288// https://utcc.utoronto.ca/~cks/space/blog/linux/NFSMountstatsIndex
289#[derive(Debug, Clone)]
290#[cfg_attr(test, derive(PartialEq))]
291#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
292pub struct MountNFSStatistics {
293    /// The version of the NFS statistics block.  Either "1.0" or "1.1".
294    pub version: String,
295    /// The mount options.
296    ///
297    /// The meaning of these can be found in the manual pages for mount(5) and nfs(5)
298    pub opts: Vec<String>,
299    /// Duration the NFS mount has been in existence.
300    pub age: Duration,
301    // * fsc (?)
302    // * impl_id (NFSv4): Option<HashMap<String, Some(String)>>
303    /// NFS Capabilities.
304    ///
305    /// See `include/linux/nfs_fs_sb.h`
306    ///
307    /// Some known values:
308    /// * caps: server capabilities.  See [NFSServerCaps].
309    /// * wtmult: server disk block size
310    /// * dtsize: readdir size
311    /// * bsize: server block size
312    pub caps: Vec<String>,
313    // * nfsv4 (NFSv4): Option<HashMap<String, Some(String)>>
314    pub sec: Vec<String>,
315    pub events: NFSEventCounter,
316    pub bytes: NFSByteCounter,
317    // * RPC iostats version:
318    // * xprt
319    // * per-op statistics
320    pub per_op_stats: NFSPerOpStats,
321}
322
323impl MountNFSStatistics {
324    // Keep reading lines until we get to a blank line
325    fn from_lines<B: BufRead>(r: &mut Lines<B>, statsver: &str) -> ProcResult<MountNFSStatistics> {
326        let mut parsing_per_op = false;
327
328        let mut opts: Option<Vec<String>> = None;
329        let mut age = None;
330        let mut caps = None;
331        let mut sec = None;
332        let mut bytes = None;
333        let mut events = None;
334        let mut per_op = HashMap::new();
335
336        while let Some(Ok(line)) = r.next() {
337            let line = line.trim();
338            if line.trim() == "" {
339                break;
340            }
341            if !parsing_per_op {
342                if let Some(stripped) = line.strip_prefix("opts:") {
343                    opts = Some(stripped.trim().split(',').map(|s| s.to_string()).collect());
344                } else if let Some(stripped) = line.strip_prefix("age:") {
345                    age = Some(Duration::from_secs(from_str!(u64, stripped.trim())));
346                } else if let Some(stripped) = line.strip_prefix("caps:") {
347                    caps = Some(stripped.trim().split(',').map(|s| s.to_string()).collect());
348                } else if let Some(stripped) = line.strip_prefix("sec:") {
349                    sec = Some(stripped.trim().split(',').map(|s| s.to_string()).collect());
350                } else if let Some(stripped) = line.strip_prefix("bytes:") {
351                    bytes = Some(NFSByteCounter::from_str(stripped.trim())?);
352                } else if let Some(stripped) = line.strip_prefix("events:") {
353                    events = Some(NFSEventCounter::from_str(stripped.trim())?);
354                }
355                if line == "per-op statistics" {
356                    parsing_per_op = true;
357                }
358            } else {
359                let mut split = line.split(':');
360                let name = expect!(split.next()).to_string();
361                let stats = NFSOperationStat::from_str(expect!(split.next()))?;
362                per_op.insert(name, stats);
363            }
364        }
365
366        Ok(MountNFSStatistics {
367            version: statsver.to_string(),
368            opts: expect!(opts, "Failed to find opts field in nfs stats"),
369            age: expect!(age, "Failed to find age field in nfs stats"),
370            caps: expect!(caps, "Failed to find caps field in nfs stats"),
371            sec: expect!(sec, "Failed to find sec field in nfs stats"),
372            events: expect!(events, "Failed to find events section in nfs stats"),
373            bytes: expect!(bytes, "Failed to find bytes section in nfs stats"),
374            per_op_stats: per_op,
375        })
376    }
377
378    /// Attempts to parse the caps= value from the [caps](struct.MountNFSStatistics.html#structfield.caps) field.
379    pub fn server_caps(&self) -> ProcResult<Option<NFSServerCaps>> {
380        for data in &self.caps {
381            if let Some(stripped) = data.strip_prefix("caps=0x") {
382                let val = from_str!(u32, stripped, 16);
383                return Ok(NFSServerCaps::from_bits(val));
384            }
385        }
386        Ok(None)
387    }
388}
389
390/// Represents NFS data from `/proc/<pid>/mountstats` under the section `events`.
391///
392/// The underlying data structure in the kernel can be found under *fs/nfs/iostat.h* `nfs_iostat`.
393/// The fields are documented in the kernel source only under *include/linux/nfs_iostat.h* `enum
394/// nfs_stat_eventcounters`.
395#[derive(Debug, Copy, Clone)]
396#[cfg_attr(test, derive(PartialEq))]
397#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
398pub struct NFSEventCounter {
399    pub inode_revalidate: u64,
400    pub deny_try_revalidate: u64,
401    pub data_invalidate: u64,
402    pub attr_invalidate: u64,
403    pub vfs_open: u64,
404    pub vfs_lookup: u64,
405    pub vfs_access: u64,
406    pub vfs_update_page: u64,
407    pub vfs_read_page: u64,
408    pub vfs_read_pages: u64,
409    pub vfs_write_page: u64,
410    pub vfs_write_pages: u64,
411    pub vfs_get_dents: u64,
412    pub vfs_set_attr: u64,
413    pub vfs_flush: u64,
414    pub vfs_fs_sync: u64,
415    pub vfs_lock: u64,
416    pub vfs_release: u64,
417    pub congestion_wait: u64,
418    pub set_attr_trunc: u64,
419    pub extend_write: u64,
420    pub silly_rename: u64,
421    pub short_read: u64,
422    pub short_write: u64,
423    pub delay: u64,
424    pub pnfs_read: u64,
425    pub pnfs_write: u64,
426}
427
428impl NFSEventCounter {
429    fn from_str(s: &str) -> ProcResult<NFSEventCounter> {
430        let mut s = s.split_whitespace();
431        Ok(NFSEventCounter {
432            inode_revalidate: from_str!(u64, expect!(s.next())),
433            deny_try_revalidate: from_str!(u64, expect!(s.next())),
434            data_invalidate: from_str!(u64, expect!(s.next())),
435            attr_invalidate: from_str!(u64, expect!(s.next())),
436            vfs_open: from_str!(u64, expect!(s.next())),
437            vfs_lookup: from_str!(u64, expect!(s.next())),
438            vfs_access: from_str!(u64, expect!(s.next())),
439            vfs_update_page: from_str!(u64, expect!(s.next())),
440            vfs_read_page: from_str!(u64, expect!(s.next())),
441            vfs_read_pages: from_str!(u64, expect!(s.next())),
442            vfs_write_page: from_str!(u64, expect!(s.next())),
443            vfs_write_pages: from_str!(u64, expect!(s.next())),
444            vfs_get_dents: from_str!(u64, expect!(s.next())),
445            vfs_set_attr: from_str!(u64, expect!(s.next())),
446            vfs_flush: from_str!(u64, expect!(s.next())),
447            vfs_fs_sync: from_str!(u64, expect!(s.next())),
448            vfs_lock: from_str!(u64, expect!(s.next())),
449            vfs_release: from_str!(u64, expect!(s.next())),
450            congestion_wait: from_str!(u64, expect!(s.next())),
451            set_attr_trunc: from_str!(u64, expect!(s.next())),
452            extend_write: from_str!(u64, expect!(s.next())),
453            silly_rename: from_str!(u64, expect!(s.next())),
454            short_read: from_str!(u64, expect!(s.next())),
455            short_write: from_str!(u64, expect!(s.next())),
456            delay: from_str!(u64, expect!(s.next())),
457            pnfs_read: from_str!(u64, expect!(s.next())),
458            pnfs_write: from_str!(u64, expect!(s.next())),
459        })
460    }
461}
462
463/// Represents NFS data from `/proc/<pid>/mountstats` under the section `bytes`.
464///
465/// The underlying data structure in the kernel can be found under *fs/nfs/iostat.h* `nfs_iostat`.
466/// The fields are documented in the kernel source only under *include/linux/nfs_iostat.h* `enum
467/// nfs_stat_bytecounters`
468#[derive(Debug, Copy, Clone)]
469#[cfg_attr(test, derive(PartialEq))]
470#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
471pub struct NFSByteCounter {
472    pub normal_read: u64,
473    pub normal_write: u64,
474    pub direct_read: u64,
475    pub direct_write: u64,
476    pub server_read: u64,
477    pub server_write: u64,
478    pub pages_read: u64,
479    pub pages_write: u64,
480}
481
482impl NFSByteCounter {
483    fn from_str(s: &str) -> ProcResult<NFSByteCounter> {
484        let mut s = s.split_whitespace();
485        Ok(NFSByteCounter {
486            normal_read: from_str!(u64, expect!(s.next())),
487            normal_write: from_str!(u64, expect!(s.next())),
488            direct_read: from_str!(u64, expect!(s.next())),
489            direct_write: from_str!(u64, expect!(s.next())),
490            server_read: from_str!(u64, expect!(s.next())),
491            server_write: from_str!(u64, expect!(s.next())),
492            pages_read: from_str!(u64, expect!(s.next())),
493            pages_write: from_str!(u64, expect!(s.next())),
494        })
495    }
496}
497
498/// Represents NFS data from `/proc/<pid>/mountstats` under the section of `per-op statistics`.
499///
500/// Here is what the Kernel says about the attributes:
501///
502/// Regarding `operations`, `transmissions` and `major_timeouts`:
503///
504/// >  These counters give an idea about how many request
505/// >  transmissions are required, on average, to complete that
506/// >  particular procedure.  Some procedures may require more
507/// >  than one transmission because the server is unresponsive,
508/// >  the client is retransmitting too aggressively, or the
509/// >  requests are large and the network is congested.
510///
511/// Regarding `bytes_sent` and `bytes_recv`:
512///
513/// >  These count how many bytes are sent and received for a
514/// >  given RPC procedure type.  This indicates how much load a
515/// >  particular procedure is putting on the network.  These
516/// >  counts include the RPC and ULP headers, and the request
517/// >  payload.
518///
519/// Regarding `cum_queue_time`, `cum_resp_time` and `cum_total_req_time`:
520///
521/// >  The length of time an RPC request waits in queue before
522/// >  transmission, the network + server latency of the request,
523/// >  and the total time the request spent from init to release
524/// >  are measured.
525///
526/// (source: *include/linux/sunrpc/metrics.h* `struct rpc_iostats`)
527#[derive(Debug, Clone)]
528#[cfg_attr(test, derive(PartialEq))]
529#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
530pub struct NFSOperationStat {
531    /// Count of rpc operations.
532    pub operations: u64,
533    /// Count of rpc transmissions
534    pub transmissions: u64,
535    /// Count of rpc major timeouts
536    pub major_timeouts: u64,
537    /// Count of bytes send. Does not only include the RPC payload but the RPC headers as well.
538    pub bytes_sent: u64,
539    /// Count of bytes received as `bytes_sent`.
540    pub bytes_recv: u64,
541    /// How long all requests have spend in the queue before being send.
542    pub cum_queue_time: Duration,
543    /// How long it took to get a response back.
544    pub cum_resp_time: Duration,
545    /// How long all requests have taken from beeing queued to the point they where completely
546    /// handled.
547    pub cum_total_req_time: Duration,
548}
549
550impl NFSOperationStat {
551    fn from_str(s: &str) -> ProcResult<NFSOperationStat> {
552        let mut s = s.split_whitespace();
553
554        let operations = from_str!(u64, expect!(s.next()));
555        let transmissions = from_str!(u64, expect!(s.next()));
556        let major_timeouts = from_str!(u64, expect!(s.next()));
557        let bytes_sent = from_str!(u64, expect!(s.next()));
558        let bytes_recv = from_str!(u64, expect!(s.next()));
559        let cum_queue_time_ms = from_str!(u64, expect!(s.next()));
560        let cum_resp_time_ms = from_str!(u64, expect!(s.next()));
561        let cum_total_req_time_ms = from_str!(u64, expect!(s.next()));
562
563        Ok(NFSOperationStat {
564            operations,
565            transmissions,
566            major_timeouts,
567            bytes_sent,
568            bytes_recv,
569            cum_queue_time: Duration::from_millis(cum_queue_time_ms),
570            cum_resp_time: Duration::from_millis(cum_resp_time_ms),
571            cum_total_req_time: Duration::from_millis(cum_total_req_time_ms),
572        })
573    }
574}
575
576pub type NFSPerOpStats = HashMap<String, NFSOperationStat>;
577
578#[cfg(test)]
579mod tests {
580    use super::*;
581    use crate::FromRead;
582    use std::time::Duration;
583
584    #[test]
585    fn test_mountinfo() {
586        let s = "25 0 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro";
587
588        let stat = MountInfo::from_line(s).unwrap();
589        println!("{:?}", stat);
590    }
591
592    #[test]
593    fn test_proc_mountstats() {
594        let MountStats(simple) = FromRead::from_read(
595            "device /dev/md127 mounted on /boot with fstype ext2 
596device /dev/md124 mounted on /home with fstype ext4 
597device tmpfs mounted on /run/user/0 with fstype tmpfs 
598"
599            .as_bytes(),
600        )
601        .unwrap();
602        let simple_parsed = vec![
603            MountStat {
604                device: Some("/dev/md127".to_string()),
605                mount_point: PathBuf::from("/boot"),
606                fs: "ext2".to_string(),
607                statistics: None,
608            },
609            MountStat {
610                device: Some("/dev/md124".to_string()),
611                mount_point: PathBuf::from("/home"),
612                fs: "ext4".to_string(),
613                statistics: None,
614            },
615            MountStat {
616                device: Some("tmpfs".to_string()),
617                mount_point: PathBuf::from("/run/user/0"),
618                fs: "tmpfs".to_string(),
619                statistics: None,
620            },
621        ];
622        assert_eq!(simple, simple_parsed);
623        let MountStats(mountstats) = FromRead::from_read("device elwe:/space mounted on /srv/elwe/space with fstype nfs4 statvers=1.1 
624       opts:   rw,vers=4.1,rsize=131072,wsize=131072,namlen=255,acregmin=3,acregmax=60,acdirmin=30,acdirmax=60,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=krb5,clientaddr=10.0.1.77,local_lock=none 
625       age:    3542 
626       impl_id:        name='',domain='',date='0,0' 
627       caps:   caps=0x3ffdf,wtmult=512,dtsize=32768,bsize=0,namlen=255 
628       nfsv4:  bm0=0xfdffbfff,bm1=0x40f9be3e,bm2=0x803,acl=0x3,sessions,pnfs=not configured 
629       sec:    flavor=6,pseudoflavor=390003 
630       events: 114 1579 5 3 132 20 3019 1 2 3 4 5 115 1 4 1 2 4 3 4 5 6 7 8 9 0 1  
631       bytes:  1 2 3 4 5 6 7 8  
632       RPC iostats version: 1.0  p/v: 100003/4 (nfs) 
633       xprt:   tcp 909 0 1 0 2 294 294 0 294 0 2 0 0 
634       per-op statistics 
635               NULL: 0 0 0 0 0 0 0 0 
636               READ: 1 2 3 4 5 6 7 8 
637              WRITE: 0 0 0 0 0 0 0 0 
638             COMMIT: 0 0 0 0 0 0 0 0 
639               OPEN: 1 1 0 320 420 0 124 124 
640        ".as_bytes()).unwrap();
641        let nfs_v4 = &mountstats[0];
642        match &nfs_v4.statistics {
643            Some(stats) => {
644                assert_eq!("1.1".to_string(), stats.version, "mountstats version wrongly parsed.");
645                assert_eq!(Duration::from_secs(3542), stats.age);
646                assert_eq!(1, stats.bytes.normal_read);
647                assert_eq!(114, stats.events.inode_revalidate);
648                assert!(stats.server_caps().unwrap().is_some());
649            }
650            None => {
651                panic!("Failed to retrieve nfs statistics");
652            }
653        }
654    }
655}