#![warn(missing_docs)]
#![forbid(unsafe_code)]
pub mod config;
pub use crate::config::Config;
use anyhow::{anyhow, Result};
use fs2::FileExt;
use log::{trace, warn};
use daemonize::Daemonize;
use spin_sleep::LoopHelper;
use std::{
env,
io::{self, BufWriter, Write},
path::Path,
};
use std::{
fs::{rename, OpenOptions},
process::{Command, Stdio},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use tempfile::NamedTempFile;
pub trait MuninPlugin {
fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()>;
fn acquire<W: Write>(
&mut self,
handle: &mut BufWriter<W>,
config: &Config,
epoch: u64,
) -> Result<()>;
#[cfg(not(tarpaulin_include))]
fn daemon(&mut self, config: &Config) -> Result<()> {
let daemonize = Daemonize::new()
.pid_file(&config.pidfile)
.chown_pid_file(true)
.working_directory("/tmp");
daemonize.start()?;
let mut loop_helper = LoopHelper::builder().build_with_target_rate(1);
loop {
loop_helper.loop_start();
let epoch = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
{
let mut handle = BufWriter::with_capacity(
config.fetch_size,
OpenOptions::new()
.create(true) .write(true) .append(true) .open(&config.plugin_cache)?,
);
self.acquire(&mut handle, config, epoch)?;
}
loop_helper.loop_sleep();
}
}
fn fetch<W: Write>(&mut self, handle: &mut BufWriter<W>, config: &Config) -> Result<()> {
if config.daemonize {
let fetchpath = NamedTempFile::new_in(&config.plugin_statedir)?;
rename(&config.plugin_cache, &fetchpath)?;
let mut fetchfile = std::fs::File::open(&fetchpath)?;
io::copy(&mut fetchfile, handle)?;
} else {
self.acquire(handle, config, 0)?;
}
Ok(())
}
fn check_autoconf(&self) -> bool {
false
}
#[cfg(not(tarpaulin_include))]
fn autoconf(&self) {
if self.check_autoconf() {
println!("yes")
} else {
println!("no")
}
}
#[cfg(not(tarpaulin_include))]
fn simple_start(&mut self, name: String) -> Result<bool> {
trace!("Simple Start, setting up config");
let config = Config::new(name);
trace!("Plugin: {:#?}", config);
self.start(config)?;
Ok(true)
}
#[cfg(not(tarpaulin_include))]
fn start(&mut self, config: Config) -> Result<bool> {
trace!("Plugin start");
trace!("My plugin config: {config:#?}");
let args: Vec<String> = env::args().collect();
match args.len() {
1 => {
trace!("No argument, assuming fetch");
if config.daemonize {
let lockfile = !Path::exists(&config.pidfile) || {
let lockedfile = OpenOptions::new()
.create(true)
.write(true)
.open(&config.pidfile)?;
lockedfile.try_lock_exclusive().is_ok()
};
if lockfile {
trace!("Could lock the pidfile, will spawn acquire now");
Command::new(&args[0])
.arg("acquire")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
trace!("Spawned, sleep for 1s, then continue");
thread::sleep(Duration::from_secs(1));
}
}
trace!("Calling fetch");
let stdout = io::stdout();
let mut handle = BufWriter::with_capacity(config.fetch_size, stdout.lock());
self.fetch(&mut handle, &config)?;
trace!("Done");
handle.flush()?;
return Ok(true);
}
2 => match args[1].as_str() {
"config" => {
let stdout = io::stdout();
{
let mut handle =
BufWriter::with_capacity(config.config_size, stdout.lock());
self.config(&mut handle)?;
handle.flush()?;
}
if config.dirtyconfig {
trace!("Munin supports dirtyconfig, sending data now");
let mut handle = BufWriter::with_capacity(config.fetch_size, stdout.lock());
self.fetch(&mut handle, &config)?;
handle.flush()?;
}
return Ok(true);
}
"autoconf" => {
self.autoconf();
return Ok(true);
}
"acquire" => {
trace!("Called acquire to gather data, will run loop forever");
if let Err(e) = self.daemon(&config) {
return Err(anyhow!(
"Could not start plugin {} in daemon mode to gather data - already running? ({})",
config.plugin_name,
e
));
};
}
&_ => trace!("Unsupported argument: {}", args[1]),
},
_ => return Err(anyhow!("No argument given")),
}
Ok(true)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TestPlugin;
impl MuninPlugin for TestPlugin {
fn config<W: Write>(&self, handle: &mut BufWriter<W>) -> Result<()> {
writeln!(handle, "This is a test plugin")?;
writeln!(handle, "There is no config")?;
Ok(())
}
fn acquire<W: Write>(
&mut self,
handle: &mut BufWriter<W>,
config: &Config,
epoch: u64,
) -> Result<()> {
writeln!(handle, "This is a value for {}", config.plugin_name)?;
writeln!(handle, "And one more value with epoch {}", epoch)?;
Ok(())
}
}
#[test]
fn test_config() {
let test = TestPlugin;
let checktext = Vec::new();
let mut handle = BufWriter::new(checktext);
test.config(&mut handle).unwrap();
handle.flush().unwrap();
let (recovered_writer, _buffered_data) = handle.into_parts();
let output = String::from_utf8(recovered_writer).unwrap();
assert_eq!(
output,
String::from("This is a test plugin\nThere is no config\n")
);
}
#[test]
fn test_fetch_standard() {
let mut test = TestPlugin;
let checktext = Vec::new();
let mut handle = BufWriter::new(checktext);
test.fetch(&mut handle, &config::Config::new("test".to_string()))
.unwrap();
handle.flush().unwrap();
let (recovered_writer, _buffered_data) = handle.into_parts();
let output = String::from_utf8(recovered_writer).unwrap();
assert_eq!(
output,
String::from("This is a value for test\nAnd one more value with epoch 0\n")
);
}
#[test]
fn test_fetch_streaming() {
let mut config = Config::new(String::from("testplugin"));
config.daemonize = true;
config.fetch_size = 16384;
let mut test = TestPlugin {};
let fetchpath = NamedTempFile::new_in(
config
.plugin_cache
.parent()
.expect("Could not find useful temp path"),
)
.unwrap();
{
let mut handle = BufWriter::with_capacity(
config.fetch_size,
OpenOptions::new()
.create(true) .write(true) .append(true) .open(&fetchpath)
.unwrap(),
);
test.acquire(&mut handle, &config, 42).unwrap();
}
(_, config.plugin_cache) = fetchpath.keep().unwrap();
let checktext = Vec::new();
let mut handle = BufWriter::new(checktext);
test.fetch(&mut handle, &config).unwrap();
handle.flush().unwrap();
let (recovered_writer, _buffered_data) = handle.into_parts();
let output = String::from_utf8(recovered_writer).unwrap();
assert_eq!(
output,
String::from("This is a value for testplugin\nAnd one more value with epoch 42\n")
);
}
#[test]
fn test_check_autoconf() {
let test = TestPlugin;
assert!(!test.check_autoconf());
}
}