From 636f7dd4089f8aa3a5051e0c84114ccc856e8bb5 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Fri, 17 Jan 2020 19:15:13 -0600 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 277 +++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 19 ++++ README.md | 73 +++++++++++++ rustfmt.toml | 1 + src/cache.rs | 108 ++++++++++++++++++ src/error.rs | 51 +++++++++ src/main.rs | 165 ++++++++++++++++++++++++++++ src/mountinfo.rs | 144 ++++++++++++++++++++++++ 9 files changed, 839 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 rustfmt.toml create mode 100644 src/cache.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/mountinfo.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b7c73ac --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,277 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "clap" +version = "2.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", + "vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "hermit-abi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.66" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libudev" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "libudev-sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mizule" +version = "0.1.0" +dependencies = [ + "chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", + "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libudev 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "ryu" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "unicode-xid" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "vec_map" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +"checksum chrono 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)" = "31850b4a4d6bae316f7a09e691c944c28299298837edc0a03f755618c23cbc01" +"checksum clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5067f5bb2d80ef5d68b4c87db81601f0b75bca627bc2ef76b141d7b846a3c6d9" +"checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772" +"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" +"checksum libudev 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ea626d3bdf40a1c5aee3bcd4f40826970cae8d80a8fec934c82a63840094dcfe" +"checksum libudev-sys 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +"checksum num-integer 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "3f6ea62e9d81a77cd3ee9a2a5b9b609447857f3d358704331e4ef39eb247fcba" +"checksum num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "c62be47e61d1842b9170f0fdeec8eba98e60e90e5446449a0545e5152acd7096" +"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677" +"checksum proc-macro2 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3acb317c6ff86a4e579dfa00fc5e6cca91ecbb4e7eb2df0468805b674eb88548" +"checksum quote 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "053a8c8bcc71fcce321828dc897a98ab9760bef03a4fc36693c231e5b3216cfe" +"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" +"checksum ryu 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bfa8506c1de11c9c4e4c38863ccbe02a305c8188e85a05a784c9e11e1c3910c8" +"checksum serde 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "414115f25f818d7dfccec8ee535d76949ae78584fc4f79a6f45a904bf8ab4449" +"checksum serde_derive 1.0.104 (registry+https://github.com/rust-lang/crates.io-index)" = "128f9e303a5a29922045a830221b8f78ec74a5f544944f3d5984f8ec3895ef64" +"checksum serde_json 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)" = "48c575e0cc52bdd09b47f330f646cf59afc586e9c4e3ccd6fc1f625b8ea1dad7" +"checksum strsim 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +"checksum syn 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "1e4ff033220a41d1a57d8125eab57bf5263783dfdcc18688b1dacc6ce9651ef8" +"checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +"checksum time 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "db8dcfca086c1143c9270ac42a2bbd8a7ee477b78ac8e45b19abfb0cbede4b6f" +"checksum unicode-width 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "caaa9d531767d1ff2150b9332433f32a24622147e5ebb1f26409d5da67afd479" +"checksum unicode-xid 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "826e7639553986605ec5979c7dd957c7895e93eabed50ab2ffa7f6128a75097c" +"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a" +"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c1f2c30 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "mizule" +version = "0.1.0" +authors = [ + "Dustin C. Hatch ", +] + +[dependencies] +clap = "2" +libudev = "0.2" +serde_json = "1.0" + +[dependencies.chrono] +version = "0.4" +features = ["serde"] + +[dependencies.serde] +version = "1.0" +features = ["derive"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7912dc --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# Mizule + +*Mizule* is a simple tool that checks the UUID of the filesystem mounted at +the specified path, and issues a warning if it has not changed within a given +time period (default 30 days). This is useful, for example, to remind backup +operators to switch out the backup disk periodically. + +## Usage + +Check `/var/spool/burp` and warn if it has not changed in the last 30 days: + +```sh +mizule /var/spool/burp +``` + +Check `/var/spool/burp` and warn if it has not changed in the last 10 days, +sending an email to `burp-admin@example.org`: + +```sh +mizule /var/spool/burp --ttl 30 --mailto burp-admin@example.org +``` + +## Cache File + +*Mizule* keeps track of the UUID of each of the filesystems it has seen in a +cache file. This file is stored at `${XDG_CACHE_HOME}/mizule.json`. If the +`XDG_CACHE_HOME` environment variable is not set, `${HOME}/.cache` is used. If +the `HOME` environment variable is also not set, the cache file will be created +in the current working directory. + + +## Periodic Check with systemd Timer Unit + +*Mizule* works best if it is scheduled to check the filesystem periodically. +One way to set up this schedule is to use a systemd timer unit. + +`mizule@.service`: + +```ini +[Unit] +Description=Check last filesystem change for %I +RequiresMountsFor=%I +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +Environment=XDG_CACHE_HOME=/var/cache/mizule +ExecStart=/usr/local/bin/mizule %I --mailto burp-admin@example.org +``` + +`mizule@.timer`: + +```ini +[Unit] +Description=Schedule filesystem check for %I + +[Timer] +OnBootSec=5m +OnUnitInactiveSec=12h + +[Install] +WantedBy=multi-user.target +``` + +To enable: + +```sh +systemctl enable mizule@-var-spool-burp.timer +``` + +This will trigger the check 5 minutes after the machine boots, and then again +every 12 hours. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..df99c69 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..533e27a --- /dev/null +++ b/src/cache.rs @@ -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, +} + +impl CacheEntry { + pub fn changed(&self) -> chrono::DateTime { + 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, +} + +#[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 for Error { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +impl From 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 { + 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(()) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..d1ea291 --- /dev/null +++ b/src/error.rs @@ -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 for Error { + fn from(err: io::Error) -> Self { + Self::Io(err) + } +} + +impl From for Error { + fn from(err: libudev::Error) -> Self { + Self::Udev(err) + } +} + +impl From 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()), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6f4ccc4 --- /dev/null +++ b/src/main.rs @@ -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 = 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, + 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(()) +} diff --git a/src/mountinfo.rs b/src/mountinfo.rs new file mode 100644 index 0000000..2ad201e --- /dev/null +++ b/src/mountinfo.rs @@ -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, +} + +impl Iterator for MountInfoIterator { + type Item = MountInfo; + + fn next(&mut self) -> Option { + 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 { + let file = fs::File::open("/proc/self/mountinfo")?; + Ok(MountInfoIterator { + reader: io::BufReader::new(file), + }) +} + +pub fn get_mountinfo(mountpoint: &str) -> Option { + 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 { + 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) +}