Initial commit
commit
636f7dd408
|
@ -0,0 +1 @@
|
||||||
|
target/
|
|
@ -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"
|
|
@ -0,0 +1,19 @@
|
||||||
|
[package]
|
||||||
|
name = "mizule"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
"Dustin C. Hatch <dustin@hatch.name>",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = "2"
|
||||||
|
libudev = "0.2"
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[dependencies.chrono]
|
||||||
|
version = "0.4"
|
||||||
|
features = ["serde"]
|
||||||
|
|
||||||
|
[dependencies.serde]
|
||||||
|
version = "1.0"
|
||||||
|
features = ["derive"]
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
max_width = 80
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue