use bitflags::bitflags;
use crate::{from_iter, ProcResult};
use std::collections::HashMap;
use std::io::{BufRead, Lines};
use std::path::PathBuf;
use std::time::Duration;
#[cfg(feature = "serde1")]
use serde::{Deserialize, Serialize};
bitflags! {
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub struct NFSServerCaps: u32 {
const NFS_CAP_READDIRPLUS = 1;
const NFS_CAP_HARDLINKS = (1 << 1);
const NFS_CAP_SYMLINKS = (1 << 2);
const NFS_CAP_ACLS = (1 << 3);
const NFS_CAP_ATOMIC_OPEN = (1 << 4);
const NFS_CAP_LGOPEN = (1 << 5);
const NFS_CAP_FILEID = (1 << 6);
const NFS_CAP_MODE = (1 << 7);
const NFS_CAP_NLINK = (1 << 8);
const NFS_CAP_OWNER = (1 << 9);
const NFS_CAP_OWNER_GROUP = (1 << 10);
const NFS_CAP_ATIME = (1 << 11);
const NFS_CAP_CTIME = (1 << 12);
const NFS_CAP_MTIME = (1 << 13);
const NFS_CAP_POSIX_LOCK = (1 << 14);
const NFS_CAP_UIDGID_NOMAP = (1 << 15);
const NFS_CAP_STATEID_NFSV41 = (1 << 16);
const NFS_CAP_ATOMIC_OPEN_V1 = (1 << 17);
const NFS_CAP_SECURITY_LABEL = (1 << 18);
const NFS_CAP_SEEK = (1 << 19);
const NFS_CAP_ALLOCATE = (1 << 20);
const NFS_CAP_DEALLOCATE = (1 << 21);
const NFS_CAP_LAYOUTSTATS = (1 << 22);
const NFS_CAP_CLONE = (1 << 23);
const NFS_CAP_COPY = (1 << 24);
const NFS_CAP_OFFLOAD_CANCEL = (1 << 25);
}
}
pub struct MountInfos(pub Vec<MountInfo>);
impl MountInfos {
pub fn iter(&self) -> std::slice::Iter<'_, MountInfo> {
self.into_iter()
}
}
impl crate::FromBufRead for MountInfos {
fn from_buf_read<R: BufRead>(r: R) -> ProcResult<Self> {
let lines = r.lines();
let mut vec = Vec::new();
for line in lines {
vec.push(MountInfo::from_line(&line?)?);
}
Ok(MountInfos(vec))
}
}
impl IntoIterator for MountInfos {
type IntoIter = std::vec::IntoIter<MountInfo>;
type Item = MountInfo;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> IntoIterator for &'a MountInfos {
type IntoIter = std::slice::Iter<'a, MountInfo>;
type Item = &'a MountInfo;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct MountInfo {
pub mnt_id: i32,
pub pid: i32,
pub majmin: String,
pub root: String,
pub mount_point: PathBuf,
pub mount_options: HashMap<String, Option<String>>,
pub opt_fields: Vec<MountOptFields>,
pub fs_type: String,
pub mount_source: Option<String>,
pub super_options: HashMap<String, Option<String>>,
}
impl MountInfo {
pub fn from_line(line: &str) -> ProcResult<MountInfo> {
let mut split = line.split_whitespace();
let mnt_id = expect!(from_iter(&mut split));
let pid = expect!(from_iter(&mut split));
let majmin: String = expect!(from_iter(&mut split));
let root = expect!(from_iter(&mut split));
let mount_point = expect!(from_iter(&mut split));
let mount_options = {
let mut map = HashMap::new();
let all_opts = expect!(split.next());
for opt in all_opts.split(',') {
let mut s = opt.splitn(2, '=');
let opt_name = expect!(s.next());
map.insert(opt_name.to_owned(), s.next().map(|s| s.to_owned()));
}
map
};
let mut opt_fields = Vec::new();
loop {
let f = expect!(split.next());
if f == "-" {
break;
}
let mut s = f.split(':');
let opt = match expect!(s.next()) {
"shared" => {
let val = expect!(from_iter(&mut s));
MountOptFields::Shared(val)
}
"master" => {
let val = expect!(from_iter(&mut s));
MountOptFields::Master(val)
}
"propagate_from" => {
let val = expect!(from_iter(&mut s));
MountOptFields::PropagateFrom(val)
}
"unbindable" => MountOptFields::Unbindable,
_ => continue,
};
opt_fields.push(opt);
}
let fs_type: String = expect!(from_iter(&mut split));
let mount_source = match expect!(split.next()) {
"none" => None,
x => Some(x.to_owned()),
};
let super_options = {
let mut map = HashMap::new();
let all_opts = expect!(split.next());
for opt in all_opts.split(',') {
let mut s = opt.splitn(2, '=');
let opt_name = expect!(s.next());
map.insert(opt_name.to_owned(), s.next().map(|s| s.to_owned()));
}
map
};
Ok(MountInfo {
mnt_id,
pid,
majmin,
root,
mount_point,
mount_options,
opt_fields,
fs_type,
mount_source,
super_options,
})
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub enum MountOptFields {
Shared(u32),
Master(u32),
PropagateFrom(u32),
Unbindable,
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct MountStat {
pub device: Option<String>,
pub mount_point: PathBuf,
pub fs: String,
pub statistics: Option<MountNFSStatistics>,
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct MountStats(pub Vec<MountStat>);
impl crate::FromBufRead for MountStats {
fn from_buf_read<R: BufRead>(r: R) -> ProcResult<Self> {
let mut v = Vec::new();
let mut lines = r.lines();
while let Some(Ok(line)) = lines.next() {
if line.starts_with("device ") {
let mut s = line.split_whitespace();
let device = Some(expect!(s.nth(1)).to_owned());
let mount_point = PathBuf::from(expect!(s.nth(2)));
let fs = expect!(s.nth(2)).to_owned();
let statistics = match s.next() {
Some(stats) if stats.starts_with("statvers=") => {
Some(MountNFSStatistics::from_lines(&mut lines, &stats[9..])?)
}
_ => None,
};
v.push(MountStat {
device,
mount_point,
fs,
statistics,
});
}
}
Ok(MountStats(v))
}
}
impl IntoIterator for MountStats {
type IntoIter = std::vec::IntoIter<MountStat>;
type Item = MountStat;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct MountNFSStatistics {
pub version: String,
pub opts: Vec<String>,
pub age: Duration,
pub caps: Vec<String>,
pub sec: Vec<String>,
pub events: NFSEventCounter,
pub bytes: NFSByteCounter,
pub per_op_stats: NFSPerOpStats,
}
impl MountNFSStatistics {
fn from_lines<B: BufRead>(r: &mut Lines<B>, statsver: &str) -> ProcResult<MountNFSStatistics> {
let mut parsing_per_op = false;
let mut opts: Option<Vec<String>> = None;
let mut age = None;
let mut caps = None;
let mut sec = None;
let mut bytes = None;
let mut events = None;
let mut per_op = HashMap::new();
while let Some(Ok(line)) = r.next() {
let line = line.trim();
if line.trim() == "" {
break;
}
if !parsing_per_op {
if let Some(stripped) = line.strip_prefix("opts:") {
opts = Some(stripped.trim().split(',').map(|s| s.to_string()).collect());
} else if let Some(stripped) = line.strip_prefix("age:") {
age = Some(Duration::from_secs(from_str!(u64, stripped.trim())));
} else if let Some(stripped) = line.strip_prefix("caps:") {
caps = Some(stripped.trim().split(',').map(|s| s.to_string()).collect());
} else if let Some(stripped) = line.strip_prefix("sec:") {
sec = Some(stripped.trim().split(',').map(|s| s.to_string()).collect());
} else if let Some(stripped) = line.strip_prefix("bytes:") {
bytes = Some(NFSByteCounter::from_str(stripped.trim())?);
} else if let Some(stripped) = line.strip_prefix("events:") {
events = Some(NFSEventCounter::from_str(stripped.trim())?);
}
if line == "per-op statistics" {
parsing_per_op = true;
}
} else {
let mut split = line.split(':');
let name = expect!(split.next()).to_string();
let stats = NFSOperationStat::from_str(expect!(split.next()))?;
per_op.insert(name, stats);
}
}
Ok(MountNFSStatistics {
version: statsver.to_string(),
opts: expect!(opts, "Failed to find opts field in nfs stats"),
age: expect!(age, "Failed to find age field in nfs stats"),
caps: expect!(caps, "Failed to find caps field in nfs stats"),
sec: expect!(sec, "Failed to find sec field in nfs stats"),
events: expect!(events, "Failed to find events section in nfs stats"),
bytes: expect!(bytes, "Failed to find bytes section in nfs stats"),
per_op_stats: per_op,
})
}
pub fn server_caps(&self) -> ProcResult<Option<NFSServerCaps>> {
for data in &self.caps {
if let Some(stripped) = data.strip_prefix("caps=0x") {
let val = from_str!(u32, stripped, 16);
return Ok(NFSServerCaps::from_bits(val));
}
}
Ok(None)
}
}
#[derive(Debug, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct NFSEventCounter {
pub inode_revalidate: u64,
pub deny_try_revalidate: u64,
pub data_invalidate: u64,
pub attr_invalidate: u64,
pub vfs_open: u64,
pub vfs_lookup: u64,
pub vfs_access: u64,
pub vfs_update_page: u64,
pub vfs_read_page: u64,
pub vfs_read_pages: u64,
pub vfs_write_page: u64,
pub vfs_write_pages: u64,
pub vfs_get_dents: u64,
pub vfs_set_attr: u64,
pub vfs_flush: u64,
pub vfs_fs_sync: u64,
pub vfs_lock: u64,
pub vfs_release: u64,
pub congestion_wait: u64,
pub set_attr_trunc: u64,
pub extend_write: u64,
pub silly_rename: u64,
pub short_read: u64,
pub short_write: u64,
pub delay: u64,
pub pnfs_read: u64,
pub pnfs_write: u64,
}
impl NFSEventCounter {
fn from_str(s: &str) -> ProcResult<NFSEventCounter> {
let mut s = s.split_whitespace();
Ok(NFSEventCounter {
inode_revalidate: from_str!(u64, expect!(s.next())),
deny_try_revalidate: from_str!(u64, expect!(s.next())),
data_invalidate: from_str!(u64, expect!(s.next())),
attr_invalidate: from_str!(u64, expect!(s.next())),
vfs_open: from_str!(u64, expect!(s.next())),
vfs_lookup: from_str!(u64, expect!(s.next())),
vfs_access: from_str!(u64, expect!(s.next())),
vfs_update_page: from_str!(u64, expect!(s.next())),
vfs_read_page: from_str!(u64, expect!(s.next())),
vfs_read_pages: from_str!(u64, expect!(s.next())),
vfs_write_page: from_str!(u64, expect!(s.next())),
vfs_write_pages: from_str!(u64, expect!(s.next())),
vfs_get_dents: from_str!(u64, expect!(s.next())),
vfs_set_attr: from_str!(u64, expect!(s.next())),
vfs_flush: from_str!(u64, expect!(s.next())),
vfs_fs_sync: from_str!(u64, expect!(s.next())),
vfs_lock: from_str!(u64, expect!(s.next())),
vfs_release: from_str!(u64, expect!(s.next())),
congestion_wait: from_str!(u64, expect!(s.next())),
set_attr_trunc: from_str!(u64, expect!(s.next())),
extend_write: from_str!(u64, expect!(s.next())),
silly_rename: from_str!(u64, expect!(s.next())),
short_read: from_str!(u64, expect!(s.next())),
short_write: from_str!(u64, expect!(s.next())),
delay: from_str!(u64, expect!(s.next())),
pnfs_read: from_str!(u64, expect!(s.next())),
pnfs_write: from_str!(u64, expect!(s.next())),
})
}
}
#[derive(Debug, Copy, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct NFSByteCounter {
pub normal_read: u64,
pub normal_write: u64,
pub direct_read: u64,
pub direct_write: u64,
pub server_read: u64,
pub server_write: u64,
pub pages_read: u64,
pub pages_write: u64,
}
impl NFSByteCounter {
fn from_str(s: &str) -> ProcResult<NFSByteCounter> {
let mut s = s.split_whitespace();
Ok(NFSByteCounter {
normal_read: from_str!(u64, expect!(s.next())),
normal_write: from_str!(u64, expect!(s.next())),
direct_read: from_str!(u64, expect!(s.next())),
direct_write: from_str!(u64, expect!(s.next())),
server_read: from_str!(u64, expect!(s.next())),
server_write: from_str!(u64, expect!(s.next())),
pages_read: from_str!(u64, expect!(s.next())),
pages_write: from_str!(u64, expect!(s.next())),
})
}
}
#[derive(Debug, Clone)]
#[cfg_attr(test, derive(PartialEq))]
#[cfg_attr(feature = "serde1", derive(Serialize, Deserialize))]
pub struct NFSOperationStat {
pub operations: u64,
pub transmissions: u64,
pub major_timeouts: u64,
pub bytes_sent: u64,
pub bytes_recv: u64,
pub cum_queue_time: Duration,
pub cum_resp_time: Duration,
pub cum_total_req_time: Duration,
}
impl NFSOperationStat {
fn from_str(s: &str) -> ProcResult<NFSOperationStat> {
let mut s = s.split_whitespace();
let operations = from_str!(u64, expect!(s.next()));
let transmissions = from_str!(u64, expect!(s.next()));
let major_timeouts = from_str!(u64, expect!(s.next()));
let bytes_sent = from_str!(u64, expect!(s.next()));
let bytes_recv = from_str!(u64, expect!(s.next()));
let cum_queue_time_ms = from_str!(u64, expect!(s.next()));
let cum_resp_time_ms = from_str!(u64, expect!(s.next()));
let cum_total_req_time_ms = from_str!(u64, expect!(s.next()));
Ok(NFSOperationStat {
operations,
transmissions,
major_timeouts,
bytes_sent,
bytes_recv,
cum_queue_time: Duration::from_millis(cum_queue_time_ms),
cum_resp_time: Duration::from_millis(cum_resp_time_ms),
cum_total_req_time: Duration::from_millis(cum_total_req_time_ms),
})
}
}
pub type NFSPerOpStats = HashMap<String, NFSOperationStat>;
#[cfg(test)]
mod tests {
use super::*;
use crate::FromRead;
use std::time::Duration;
#[test]
fn test_mountinfo() {
let s = "25 0 8:1 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,errors=remount-ro";
let stat = MountInfo::from_line(s).unwrap();
println!("{:?}", stat);
}
#[test]
fn test_proc_mountstats() {
let MountStats(simple) = FromRead::from_read(
"device /dev/md127 mounted on /boot with fstype ext2
device /dev/md124 mounted on /home with fstype ext4
device tmpfs mounted on /run/user/0 with fstype tmpfs
"
.as_bytes(),
)
.unwrap();
let simple_parsed = vec![
MountStat {
device: Some("/dev/md127".to_string()),
mount_point: PathBuf::from("/boot"),
fs: "ext2".to_string(),
statistics: None,
},
MountStat {
device: Some("/dev/md124".to_string()),
mount_point: PathBuf::from("/home"),
fs: "ext4".to_string(),
statistics: None,
},
MountStat {
device: Some("tmpfs".to_string()),
mount_point: PathBuf::from("/run/user/0"),
fs: "tmpfs".to_string(),
statistics: None,
},
];
assert_eq!(simple, simple_parsed);
let MountStats(mountstats) = FromRead::from_read("device elwe:/space mounted on /srv/elwe/space with fstype nfs4 statvers=1.1
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
age: 3542
impl_id: name='',domain='',date='0,0'
caps: caps=0x3ffdf,wtmult=512,dtsize=32768,bsize=0,namlen=255
nfsv4: bm0=0xfdffbfff,bm1=0x40f9be3e,bm2=0x803,acl=0x3,sessions,pnfs=not configured
sec: flavor=6,pseudoflavor=390003
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
bytes: 1 2 3 4 5 6 7 8
RPC iostats version: 1.0 p/v: 100003/4 (nfs)
xprt: tcp 909 0 1 0 2 294 294 0 294 0 2 0 0
per-op statistics
NULL: 0 0 0 0 0 0 0 0
READ: 1 2 3 4 5 6 7 8
WRITE: 0 0 0 0 0 0 0 0
COMMIT: 0 0 0 0 0 0 0 0
OPEN: 1 1 0 320 420 0 124 124
".as_bytes()).unwrap();
let nfs_v4 = &mountstats[0];
match &nfs_v4.statistics {
Some(stats) => {
assert_eq!("1.1".to_string(), stats.version, "mountstats version wrongly parsed.");
assert_eq!(Duration::from_secs(3542), stats.age);
assert_eq!(1, stats.bytes.normal_read);
assert_eq!(114, stats.events.inode_revalidate);
assert!(stats.server_caps().unwrap().is_some());
}
None => {
panic!("Failed to retrieve nfs statistics");
}
}
}
}