sys: add /proc/self/mountinfo parsing

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2020-01-14 14:41:21 +01:00
parent e128fc879f
commit 40da8098df
2 changed files with 274 additions and 0 deletions

View File

@ -15,6 +15,8 @@ use nix::unistd::Pid;
use proxmox_tools::fs::file_read_firstline;
use proxmox_tools::parse::hex_nibble;
pub mod mountinfo;
/// POSIX sysconf call
pub fn sysconf(name: i32) -> i64 {
extern "C" {

View File

@ -0,0 +1,272 @@
//! `/proc/PID/mountinfo` handling.
use std::ffi::{OsStr, OsString};
use std::os::unix::ffi::OsStrExt;
use std::path::PathBuf;
use std::str::FromStr;
use failure::{bail, format_err, Error};
use nix::sys::stat;
use nix::unistd::Pid;
/// A mount ID as found within `/proc/PID/mountinfo`.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct MountId(usize);
impl FromStr for MountId {
type Err = <usize as FromStr>::Err;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(Self)
}
}
/// A device node entry (major:minor). This is a more strongly typed version of dev_t.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct Device {
major: u32,
minor: u32,
}
impl Device {
pub fn from_dev_t(dev: stat::dev_t) -> Self {
Self {
major: stat::major(dev) as u32,
minor: stat::minor(dev) as u32,
}
}
pub fn into_dev_t(self) -> stat::dev_t {
stat::makedev(u64::from(self.major), u64::from(self.minor))
}
}
impl FromStr for Device {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Error> {
let (major, minor) = s.split_at(
s.find(':')
.ok_or_else(|| format_err!("expected 'major:minor' format"))?,
);
Ok(Self {
major: major.parse()?,
minor: minor[1..].parse()?,
})
}
}
#[derive(Clone, Debug)]
#[cfg_attr(test, derive(Eq, PartialEq))]
pub struct Tag {
pub tag: OsString,
pub value: Option<OsString>,
}
impl Tag {
fn parse(tag: &[u8]) -> Result<Self, Error> {
Ok(match tag.iter().position(|b| *b == b':') {
Some(pos) => {
let (tag, value) = tag.split_at(pos);
Self {
tag: OsStr::from_bytes(tag).to_owned(),
value: Some(OsStr::from_bytes(&value[1..]).to_owned()),
}
}
None => Self {
tag: OsStr::from_bytes(tag).to_owned(),
value: None,
},
})
}
}
pub struct Entry {
/// unique identifier of the mount (may be reused after being unmounted)
pub id: MountId,
/// id of the parent (or of self for the top of the mount tree)
pub parent: MountId,
/// value of st_dev for files on this file system
pub device: Device,
/// root of the mount within the file system
pub root: PathBuf,
/// mount point relative to the process' root
pub mount_point: PathBuf,
/// per-mount options
pub mount_options: OsString,
/// tags
pub tags: Vec<Tag>,
/// Name of the file system in the form "type[.subtype]".
pub fs_type: String,
/// File system specific mount source information.
pub mount_source: Option<OsString>,
/// superblock options
pub super_options: OsString,
}
impl Entry {
/// Parse a line from a `mountinfo` file.
pub fn parse(line: &[u8]) -> Result<Self, Error> {
let mut parts = line.split(u8::is_ascii_whitespace);
let mut next = || {
parts
.next()
.ok_or_else(|| format_err!("incomplete mountinfo line"))
};
let this = Self {
id: std::str::from_utf8(next()?)?.parse()?,
parent: std::str::from_utf8(next()?)?.parse()?,
device: std::str::from_utf8(next()?)?.parse()?,
root: OsStr::from_bytes(next()?).to_owned().into(),
mount_point: OsStr::from_bytes(next()?).to_owned().into(),
mount_options: OsStr::from_bytes(next()?).to_owned(),
tags: next()?.split(|b| *b == b',').try_fold(
Vec::new(),
|mut acc, tag| -> Result<_, Error> {
acc.push(Tag::parse(tag)?);
Ok(acc)
},
)?,
fs_type: std::str::from_utf8({
next()?;
next()?
})?
.to_string(),
mount_source: next().map(|src| match src {
b"none" => None,
other => Some(OsStr::from_bytes(other).to_owned()),
})?,
super_options: OsStr::from_bytes(next()?).to_owned(),
};
if parts.next().is_some() {
bail!("excess data in mountinfo line");
}
Ok(this)
}
}
// TODO: Add some structure to this? Eg. sort by parent/child relation? Make a tree?
/// Mount info found in `/proc/PID/mountinfo`.
pub struct MountInfo {
entries: Vec<Entry>,
}
pub type Iter<'a> = std::slice::Iter<'a, Entry>;
impl MountInfo {
/// Read the current mount point information.
pub fn read() -> Result<Self, Error> {
Self::parse(&std::fs::read("/proc/self/mountinfo")?)
}
/// Read the mount point information of a specific pid.
pub fn read_for_pid(pid: Pid) -> Result<Self, Error> {
Self::parse(&std::fs::read(format!("/proc/{}/mountinfo", pid))?)
}
/// Parse a `mountinfo` file.
pub fn parse(statstr: &[u8]) -> Result<Self, Error> {
let entries = statstr.split(|b| *b == b'\n').try_fold(
Vec::new(),
|mut acc, line| -> Result<_, Error> {
acc.push(Entry::parse(line)?);
Ok(acc)
},
)?;
Ok(Self { entries })
}
/// Iterate over mount entries.
pub fn iter(&self) -> Iter {
self.entries.iter()
}
}
#[test]
fn test_entry() {
use std::path::Path;
let l1: &[u8] =
b"48 32 0:43 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:26 - cgroup \
cgroup rw,blkio";
let entry = Entry::parse(l1).expect("failed to parse first mountinfo test entry");
assert_eq!(entry.id, MountId(48));
assert_eq!(entry.parent, MountId(32));
assert_eq!(
entry.device,
Device {
major: 0,
minor: 43,
}
);
assert_eq!(entry.root, Path::new("/"));
assert_eq!(entry.mount_point, Path::new("/sys/fs/cgroup/blkio"));
assert_eq!(
entry.mount_options,
OsStr::new("rw,nosuid,nodev,noexec,relatime")
);
assert_eq!(
entry.tags,
&[Tag {
tag: OsString::from("shared"),
value: Some(OsString::from("26")),
}]
);
assert_eq!(entry.fs_type, "cgroup");
assert_eq!(
entry.mount_source.as_ref().map(|s| s.as_os_str()),
Some(OsStr::new("cgroup"))
);
assert_eq!(entry.super_options, "rw,blkio");
let l2 = b"49 28 0:44 / /proxmox/debian rw,relatime shared:27 - autofs systemd-1 \
rw,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=27726";
let entry = Entry::parse(l2).expect("failed to parse second mountinfo test entry");
assert_eq!(entry.id, MountId(49));
assert_eq!(entry.parent, MountId(28));
assert_eq!(
entry.device,
Device {
major: 0,
minor: 44,
}
);
assert_eq!(entry.root, Path::new("/"));
assert_eq!(entry.mount_point, Path::new("/proxmox/debian"));
assert_eq!(entry.mount_options, OsStr::new("rw,relatime"));
assert_eq!(
entry.tags,
&[Tag {
tag: OsString::from("shared"),
value: Some(OsString::from("27")),
}]
);
assert_eq!(entry.fs_type, "autofs");
assert_eq!(
entry.mount_source.as_ref().map(|s| s.as_os_str()),
Some(OsStr::new("systemd-1"))
);
assert_eq!(
entry.super_options,
"rw,fd=26,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=27726"
);
let mount_info = [l1, l2].join(&b"\n"[..]);
MountInfo::parse(&mount_info).expect("failed to parse mount info file");
}