Initial commit

This commit is contained in:
2020-01-17 19:15:13 -06:00
commit 636f7dd408
9 changed files with 839 additions and 0 deletions

108
src/cache.rs Normal file
View File

@@ -0,0 +1,108 @@
use chrono::Duration;
use chrono;
use serde::{Deserialize, Serialize};
use serde_json;
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::io;
use std::io::prelude::*;
#[derive(Serialize, Deserialize, Debug)]
pub struct CacheEntry {
uuid: String,
changed: chrono::DateTime<chrono::Utc>,
}
impl CacheEntry {
pub fn changed(&self) -> chrono::DateTime<chrono::Utc> {
self.changed
}
pub fn expired(&self, ttl: Duration) -> bool {
let now = chrono::Utc::now();
now > self.changed + ttl
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Cache {
paths: HashMap<String, CacheEntry>,
}
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Json(serde_json::Error),
}
impl Error {
pub fn message(&self) -> String {
match *self {
Self::Io(ref e) => format!("{}", e),
Self::Json(ref e) => format!("{}", e),
}
}
}
impl From<io::Error> for Error {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<serde_json::Error> for Error {
fn from(error: serde_json::Error) -> Self {
Self::Json(error)
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.message())
}
}
impl Cache {
pub fn new() -> Self {
Self {
paths: HashMap::new(),
}
}
pub fn load(path: &str) -> Result<Self, Error> {
if let Ok(mut file) = fs::File::open(path) {
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let cache = serde_json::from_str(&contents);
match cache {
Ok(c) => Ok(c),
Err(e) => Err(e.into()),
}
} else {
Ok(Self::new())
}
}
pub fn get(&self, path: &str) -> Option<&CacheEntry> {
if let Some(entry) = self.paths.get(path) {
return Some(&entry);
}
None
}
pub fn update(&mut self, path: &str, uuid: &str) {
let entry = CacheEntry {
uuid: uuid.into(),
changed: chrono::Utc::now(),
};
self.paths.insert(path.into(), entry);
}
pub fn save(&self, path: &str) -> Result<(), Error> {
let mut file = fs::File::create(path)?;
let contents = serde_json::to_string(&self)?;
file.write_all(&contents.as_bytes())?;
Ok(())
}
}

51
src/error.rs Normal file
View File

@@ -0,0 +1,51 @@
use std::fmt;
use std::io;
#[derive(Debug)]
pub enum Error {
Io(io::Error),
Udev(libudev::Error),
NotMounted,
NotFound,
InvalidUuid,
}
impl Error {
pub fn message(&self) -> String {
match *self {
Self::Io(ref e) => format!("{}", e),
Self::Udev(ref e) => format!("{}", e),
Self::NotMounted => "Path is not a mount point".into(),
Self::NotFound => "Device not found".into(),
Self::InvalidUuid => "Invalid device UUID".into(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.message())
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Self {
Self::Io(err)
}
}
impl From<libudev::Error> for Error {
fn from(err: libudev::Error) -> Self {
Self::Udev(err)
}
}
impl From<Error> for io::Error {
fn from(error: Error) -> Self {
match error {
Error::Io(e) => e,
Error::Udev(e) => e.into(),
_ => io::Error::new(io::ErrorKind::Other, error.message()),
}
}
}

165
src/main.rs Normal file
View File

@@ -0,0 +1,165 @@
extern crate chrono;
extern crate clap;
extern crate libudev;
extern crate serde;
extern crate serde_json;
use chrono::Duration;
use clap::App;
use clap::Arg;
use std::env;
use std::io::prelude::*;
use std::num::ParseIntError;
use std::path::PathBuf;
use std::process;
mod cache;
mod error;
mod mountinfo;
use mountinfo::get_fs_uuid;
const CACHE_FILENAME: &'static str = "mizule.json";
fn validate_int(v: String) -> Result<(), String> {
let i: Result<i64, ParseIntError> = v.parse();
match i {
Ok(_) => Ok(()),
Err(e) => Err(format!("Invalid number: {}", e)),
}
}
fn main() {
let matches = App::new("Mizule")
.version(env!("CARGO_PKG_VERSION"))
.author(env!("CARGO_PKG_AUTHORS"))
.about("Warns when the same filesystem has been mounted too long")
.arg(
Arg::with_name("mountpoint")
.help("Path to the mounted filesystem to check")
.required(true),
)
.arg(
Arg::with_name("ttl")
.short("t")
.long("ttl")
.takes_value(true)
.validator(validate_int)
.default_value("30")
.help("Number of days before warning"),
)
.arg(
Arg::with_name("mailto")
.short("m")
.long("mailto")
.alias("mail-to")
.takes_value(true)
.help("Send warning to this email address"),
)
.get_matches();
let mountpoint = matches.value_of("mountpoint").unwrap();
let ttl: i64 = matches.value_of("ttl").unwrap().parse().unwrap();
let ttl = Duration::days(ttl);
let mailto = matches.value_of("mailto");
match get_fs_uuid(mountpoint) {
Ok(uuid) => {
check_and_notify(mountpoint, &uuid, ttl, mailto);
}
Err(e) => {
eprintln!("Error getting filesystem UUID: {}", e);
process::exit(1);
}
}
}
fn check_and_notify(
mountpoint: &str,
uuid: &str,
ttl: Duration,
mailto: Option<&str>,
) {
let mut cache_path = match env::var("XDG_CACHE_HOME") {
Ok(v) => PathBuf::from(&v),
Err(_) => match env::var("HOME") {
Ok(v) => [v, ".cache".into()].iter().collect(),
Err(_) => ".".into(),
},
};
cache_path.push(CACHE_FILENAME);
let cache_path = cache_path.to_str().unwrap();
let cache = cache::Cache::load(cache_path);
match cache {
Ok(mut cache) => {
if let Some(entry) = cache.get(&mountpoint) {
if entry.expired(ttl) {
notify(mountpoint, uuid, entry.changed(), mailto);
}
} else {
cache.update(&mountpoint, &uuid);
if let Err(e) = cache.save(&cache_path) {
eprintln!("Failed to save cache: {}", e);
}
}
}
Err(e) => {
eprintln!("Error loading cache: {}", e);
process::exit(1);
}
}
}
fn notify(
mountpoint: &str,
uuid: &str,
changed: chrono::DateTime<chrono::Utc>,
mailto: Option<&str>,
) {
let now = chrono::Utc::now();
let delta = now - changed;
let message = format!(
concat!(
"The filesystem mounted at {} (UUID {}) ",
"was last changed on {} ({} days ago)",
),
mountpoint,
uuid,
changed,
delta.num_days()
);
println!("{}", message);
if let Some(mailto) = mailto {
println!("Sending notification to {}", mailto);
let subject = format!(
"{}: {} needs to be changed!",
env!("CARGO_PKG_NAME"),
mountpoint
);
if let Err(e) = sendmail(mailto, &subject, &message) {
eprintln!("Failed to send email: {}", e);
}
}
}
fn sendmail(
mailto: &str,
subject: &str,
message: &str,
) -> Result<(), std::io::Error> {
let mut cmd = process::Command::new("sendmail")
.arg("-t")
.stdin(process::Stdio::piped())
.spawn()?;
{
let mut stdin = cmd.stdin.take().unwrap();
stdin.write_all(format!("To: {}\n", mailto).as_bytes())?;
stdin.write_all(format!("Subject: {}\n", subject).as_bytes())?;
stdin.write_all("\n".as_bytes())?;
stdin.write_all(message.as_bytes())?;
stdin.write_all("\n".as_bytes())?;
stdin.flush()?;
}
cmd.wait()?;
Ok(())
}

144
src/mountinfo.rs Normal file
View File

@@ -0,0 +1,144 @@
use std::fs;
use std::io;
use std::io::prelude::*;
use std::path;
use error::Error;
pub struct MountInfo {
pub mount_id: u32,
pub parent_id: u32,
pub major: u32,
pub minor: u32,
pub root: String,
pub mount_point: String,
pub mount_opts: String,
pub fields: String,
pub fstype: String,
pub source: String,
pub super_options: String,
}
impl MountInfo {
pub fn from_line(line: String) -> Self {
let mut parts = line.split_whitespace();
let mount_id = parts.next().unwrap().parse().expect("invalid mount id");
let parent_id =
parts.next().unwrap().parse().expect("invalid parent id");
let mut majmin = parts.next().unwrap().split(":");
let major = majmin
.next()
.unwrap()
.parse()
.expect("invalid major number");
let minor = majmin
.next()
.unwrap()
.parse()
.expect("invalid minor number");
let root = parts.next().unwrap().into();
let mount_point = parts.next().unwrap().into();
let mount_opts = parts.next().unwrap().into();
let mut fields = String::new();
loop {
let next = parts.next().unwrap();
if next == "-" {
break;
} else {
fields.push_str(" ");
fields.push_str(next);
}
}
let fstype = parts.next().unwrap().into();
let source = parts.next().unwrap().into();
let super_options = parts.next().unwrap().into();
MountInfo {
mount_id,
parent_id,
major,
minor,
root,
mount_point,
mount_opts,
fields,
fstype,
source,
super_options,
}
}
}
pub struct MountInfoIterator {
reader: io::BufReader<fs::File>,
}
impl Iterator for MountInfoIterator {
type Item = MountInfo;
fn next(&mut self) -> Option<MountInfo> {
let mut line = String::new();
let n = self.reader.read_line(&mut line);
match n {
Ok(0) => None,
Ok(_) => Some(MountInfo::from_line(line)),
Err(_) => None,
}
}
}
pub fn mountinfo() -> std::io::Result<MountInfoIterator> {
let file = fs::File::open("/proc/self/mountinfo")?;
Ok(MountInfoIterator {
reader: io::BufReader::new(file),
})
}
pub fn get_mountinfo(mountpoint: &str) -> Option<MountInfo> {
if let Ok(mounts) = mountinfo() {
for mi in mounts {
if mi.mount_point == mountpoint {
return Some(mi);
}
}
}
return None;
}
pub fn get_fs_uuid(mountpoint: &str) -> Result<String, Error> {
if let Some(mi) = get_mountinfo(mountpoint) {
let udev = libudev::Context::new()?;
let mut enumerator = libudev::Enumerator::new(&udev)?;
let mut realpath = path::PathBuf::from(&mi.source);
let stat = fs::symlink_metadata(&mi.source)?;
let file_type = stat.file_type();
if file_type.is_symlink() {
realpath.push(fs::read_link(&mi.source)?);
}
let sysname = path::Path::new(&realpath);
if let Some(sysname) = sysname.file_name() {
enumerator.match_sysname(sysname)?;
} else {
return Err(Error::NotFound);
}
for device in enumerator.scan_devices()? {
if let Some(uuid) = device.property_value("ID_FS_UUID") {
if let Some(uuid) = uuid.to_str() {
return Ok(uuid.into());
} else {
return Err(Error::InvalidUuid);
}
}
}
return Err(Error::NotFound);
}
Err(Error::NotMounted)
}