diff --git a/Cargo.toml b/Cargo.toml index bf3f585..8089823 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,8 @@ edition = "2018" [dependencies] chrono = "0.4" +inventory = "0.1" +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" -systemd = "0.4.0" +systemd = "0.4" diff --git a/src/actions/noop.rs b/src/actions/noop.rs index c26fe3a..59d2522 100644 --- a/src/actions/noop.rs +++ b/src/actions/noop.rs @@ -1,18 +1,22 @@ -use crate::modules::{Module,ModuleArgs}; +use crate::modules::{Action,AvailableAction,ModuleArgs}; use crate::common::Record; #[derive(Debug)] pub struct Noop {} +inventory::submit! { + AvailableAction::new("action_noop", move |a| Box::new(Noop::from_args(a))) +} + impl Noop { pub fn from_args(mut _args: ModuleArgs) -> Noop { Noop {} } } -impl Module for Noop { - fn run(&self, _record: &mut Record) -> Result { - Ok(true) +impl Action for Noop { + fn act(&self, _record: &mut Record) -> Result<(), ()> { + Ok(()) } } @@ -21,9 +25,9 @@ mod tests { use std::collections::HashMap; use crate::common::Record; use crate::actions::Noop; - use crate::modules::{Module,ModuleArgs}; + use crate::modules::{Action,ModuleArgs}; - fn generate_empty_args_record() -> (ModuleArgs<'static>, Record<'static>) { + fn generate_empty_args_record() -> (ModuleArgs, Record<'static>) { let args = HashMap::with_capacity(0); let record = HashMap::with_capacity(0); (args, record) @@ -33,6 +37,6 @@ mod tests { fn noop_does_nothing() { let (args, mut record) = generate_empty_args_record(); let action = Noop::from_args(args); - assert!(action.run(&mut record).unwrap()); + assert_eq!((), action.act(&mut record).unwrap()); } } diff --git a/src/common.rs b/src/common.rs index 793b9b1..cde950a 100644 --- a/src/common.rs +++ b/src/common.rs @@ -6,7 +6,9 @@ pub enum Value { Bool(bool), Str(String), Int(isize), - Date(DateTime) + Date(DateTime), + Map(HashMap), + List(Vec) } pub type Record<'a> = HashMap<&'a str, Value>; diff --git a/src/config/file.rs b/src/config/file.rs new file mode 100644 index 0000000..f790655 --- /dev/null +++ b/src/config/file.rs @@ -0,0 +1,56 @@ +use std::env; +use std::ffi::{OsString}; +use std::io::{BufReader,Error,Result}; +use std::path::Path; +use std::fs::File; + +const ENV_VARIABLE: &'static str = "PYRUSE_CONF"; + +enum ConfFile { + Json(OsString), + Yaml(OsString) +} + +pub fn from_file() { + match find_file(find_candidates()) { + ConfFile::Json(path) => super::parse_json(BufReader::new(File::open(path).expect("Read error"))), + ConfFile::Yaml(path) => super::parse_yaml(BufReader::new(File::open(path).expect("Read error"))) + }; +} + +fn find_candidates() -> Vec { + match env::var_os(ENV_VARIABLE) { + Some(path) => { + let s = Path::new(&path) + .extension() + .and_then(|e| Some(e.to_string_lossy())) + .and_then(|s| Some(s.to_ascii_lowercase())) + .unwrap_or_default(); + match s.as_ref() { + "json" => vec![ConfFile::Json(path)], + "yaml" | "yml" => vec![ConfFile::Yaml(path)], + _ => panic!("Cannot determine file format from file name: {}", path.to_string_lossy()) + } + }, + None => { + vec![ + ConfFile::Json(OsString::from("pyruse.json")), + ConfFile::Yaml(OsString::from("pyruse.yaml")), + ConfFile::Yaml(OsString::from("pyruse.yml")) + ] + } + } +} + +fn find_file(conf_candidates: Vec) -> ConfFile { + for name in conf_candidates { + match name { + ConfFile::Json(ref path) | ConfFile::Yaml(ref path) => { + if Path::new(&path).exists() { + return name; + } + } + } + } + panic!("No configuration found. Consider setting ${}, or creating one of these in $PWD: pyruse.json, pyruse.yaml or pyruse.yml", ENV_VARIABLE) +} diff --git a/src/config/mod.rs b/src/config/mod.rs index c98853b..3dc2449 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,18 +1,205 @@ -use crate::common::Record; -use crate::modules::{ModuleArgs,ModuleType}; +use serde::de::{self,Deserializer,MapAccess,SeqAccess,Visitor}; +use serde::Deserialize; +use serde_json; +use serde_yaml; +use std::cell::RefCell; +use std::collections::HashMap; +use std::fmt; +use std::io::Read; +use crate::common::Value; +use crate::modules::ModuleArgs; -pub struct Config<'a> { - actions: Vec>, - options: Record<'a> +mod file; + +thread_local!(static CONFIG: RefCell> = RefCell::new(None)); + +#[derive(Debug,Deserialize)] +pub struct Config { + actions: HashMap, + + #[serde(flatten)] + options: HashMap } -pub struct Chain<'a> { - name: String, - steps: Vec> +type Chain = Vec; + +#[derive(Debug,Deserialize)] +pub enum StepType { + #[serde(rename(deserialize = "action"))] + Action(String), + #[serde(rename(deserialize = "filter"))] + Filter(String) } -pub struct Step<'a> { - module_name: String, - module_type: ModuleType, - args: ModuleArgs<'a> +#[derive(Debug,Deserialize)] +pub struct Step { + #[serde(flatten)] + module: StepType, + args: ModuleArgs, + #[serde(rename(deserialize = "then"))] + then_dest: Option, + #[serde(rename(deserialize = "else"))] + else_dest: Option +} + +/* *** serde for Value *** */ + +struct ValueVisitor; + +impl<'de> Visitor<'de> for ValueVisitor { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a boolean, string, or integer") + } + + fn visit_bool(self, v: bool) -> Result where E: de::Error { + Ok(Value::Bool(v)) + } + + fn visit_i8(self, v: i8) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_i16(self, v: i16) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_i32(self, v: i32) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_i64(self, v: i64) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_i128(self, v: i128) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_u8(self, v: u8) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_u16(self, v: u16) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_u32(self, v: u32) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_u64(self, v: u64) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_u128(self, v: u128) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_f32(self, v: f32) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_f64(self, v: f64) -> Result where E: de::Error { + Ok(Value::Int(v as isize)) + } + + fn visit_char(self, v: char) -> Result where E: de::Error { + Ok(Value::Str(v.to_string())) + } + + fn visit_str(self, v: &str) -> Result where E: de::Error { + Ok(Value::Str(String::from(v))) + } + + fn visit_borrowed_str(self, v: &'de str) -> Result where E: de::Error { + Ok(Value::Str(String::from(v))) + } + + fn visit_string(self, v: String) -> Result where E: de::Error { + Ok(Value::Str(v)) + } + + fn visit_bytes(self, v: &[u8]) -> Result where E: de::Error { + Ok(Value::Str(std::str::from_utf8(v).expect("Strings in the configuration must be UTF-8").to_string())) + } + + fn visit_borrowed_bytes(self, v: &'de [u8]) -> Result where E: de::Error { + Ok(Value::Str(std::str::from_utf8(v).expect("Strings in the configuration must be UTF-8").to_string())) + } + + fn visit_byte_buf(self, v: Vec) -> Result where E: de::Error { + Ok(Value::Str(String::from_utf8(v).expect("Strings in the configuration must be UTF-8"))) + } + + fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de> { + let mut result = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(v) = seq.next_element()? { + result.push(v); + } + Ok(Value::List(result)) + } + + fn visit_map(self, mut map: A) -> Result where A: MapAccess<'de> { + let mut result = HashMap::with_capacity(map.size_hint().unwrap_or(0)); + while let Some((k, v)) = map.next_entry()? { + result.insert(k, v); + } + Ok(Value::Map(result)) + } +} + +impl<'de> Deserialize<'de> for Value { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + deserializer.deserialize_any(ValueVisitor) + } +} + +fn parse_json(data: impl Read) { + CONFIG.with(|config| { + config.replace(Some(serde_json::from_reader(data).expect("Failed to parse configuration"))); + }); +} + +fn parse_yaml(data: impl Read) { + CONFIG.with(|config| { + config.replace(Some(serde_yaml::from_reader(data).expect("Failed to parse configuration"))); + }); +} + +//fn handle_serde(data: se) +#[cfg(test)] +mod tests { + use super::parse_json; + + #[test] + fn parse_json_works() { + let json = r#" + { + "actions": { + "Detect request errors with Nextcloud": [ + { + "filter": "filter_equals", + "args": { "field": "SYSLOG_IDENTIFIER", "value": "uwsgi" } + }, + { + "filter": "filter_pcre", + "args": { "field": "MESSAGE", "re": "^\\[[^]]+\\] ([^ ]+) .*\\] ([A-Z]+ /[^?]*)(?:\\?.*)? => .*\\(HTTP/1.1 5..\\)", "save": [ "thatIP", "HTTPrequest" ] }, + "else": "… Report insufficient buffer-size for Nextcloud QUERY_STRING" + }, + { + "action": "action_dailyReport", + "args": { "level": "INFO", "message": "IP {thatIP} failed to {HTTPrequest} on Nextcloud", "details": "FIRSTLAST" } + } + ] + }, + "debug": false + } + "#.as_bytes(); + parse_json(json); + super::CONFIG.with(|config| { + println!("{:#?}", config.borrow()); + }); + } } diff --git a/src/filters/equals.rs b/src/filters/equals.rs index d974d63..748abe8 100644 --- a/src/filters/equals.rs +++ b/src/filters/equals.rs @@ -1,4 +1,4 @@ -use crate::modules::{Module,ModuleArgs}; +use crate::modules::{AvailableFilter,Filter,ModuleArgs}; use crate::common::Record; use crate::common::Value; @@ -8,6 +8,10 @@ pub struct Equals { value: Value } +inventory::submit! { + AvailableFilter::new("filter_equals", move |a| Box::new(Equals::from_args(a))) +} + impl Equals { pub fn from_args(mut args: ModuleArgs) -> Equals { Equals { @@ -20,11 +24,11 @@ impl Equals { } } -impl Module for Equals { - fn run(&self, record: &mut Record) -> Result { +impl Filter for Equals { + fn filter(&self, record: &mut Record) -> bool { match (record.get(&self.field.as_ref()), &self.value) { - (Some(ref v1), ref v2) => Ok(v1 == v2), - (None, _) => Ok(false) + (Some(ref v1), ref v2) => v1 == v2, + (None, _) => false } } } @@ -35,21 +39,21 @@ mod tests { use std::collections::HashMap; use crate::common::{Record,Value}; use crate::filters::Equals; - use crate::modules::{Module,ModuleArgs}; + use crate::modules::{Filter,ModuleArgs}; - fn generate_args_record_equal<'a>(name: &'a str, value: Value) -> (ModuleArgs<'static>, Record<'a>) { + fn generate_args_record_equal<'a>(name: &'a str, value: Value) -> (ModuleArgs, Record<'a>) { let mut args = HashMap::with_capacity(2); - args.insert("field", Value::Str(String::from(name))); - args.insert("value", value.clone()); + args.insert(String::from("field"), Value::Str(String::from(name))); + args.insert(String::from("value"), value.clone()); let mut record = HashMap::with_capacity(1); record.insert(name, value); (args, record) } - fn generate_args_record_custom<'a>(ref_name: &str, ref_value: Value, test_name: &'a str, test_value: Value) -> (ModuleArgs<'static>, Record<'a>) { + fn generate_args_record_custom<'a>(ref_name: &str, ref_value: Value, test_name: &'a str, test_value: Value) -> (ModuleArgs, Record<'a>) { let mut args = HashMap::with_capacity(2); - args.insert("field", Value::Str(String::from(ref_name))); - args.insert("value", ref_value); + args.insert(String::from("field"), Value::Str(String::from(ref_name))); + args.insert(String::from("value"), ref_value); let mut record = HashMap::with_capacity(1); record.insert(test_name, test_value); (args, record) @@ -59,41 +63,41 @@ mod tests { fn filter_equals_should_return_true() { let (args, mut record) = generate_args_record_equal("a_boolean", Value::Bool(false)); let filter = Equals::from_args(args); - assert!(filter.run(&mut record).unwrap()); + assert!(filter.filter(&mut record)); let (args, mut record) = generate_args_record_equal("a_string", Value::Str(String::from("Hello!"))); let filter = Equals::from_args(args); - assert!(filter.run(&mut record).unwrap()); + assert!(filter.filter(&mut record)); let (args, mut record) = generate_args_record_equal("an_integer", Value::Int(2)); let filter = Equals::from_args(args); - assert!(filter.run(&mut record).unwrap()); + assert!(filter.filter(&mut record)); let (args, mut record) = generate_args_record_equal("a_date", Value::Date(Utc::now())); let filter = Equals::from_args(args); - assert!(filter.run(&mut record).unwrap()); + assert!(filter.filter(&mut record)); } #[test] fn filter_equals_should_return_false() { let (args, mut record) = generate_args_record_custom("a_boolean", Value::Bool(true), "a_boolean", Value::Bool(false)); let filter = Equals::from_args(args); - assert!(! filter.run(&mut record).unwrap()); + assert!(! filter.filter(&mut record)); let (args, mut record) = generate_args_record_custom("a_string", Value::Str(String::from("Hello!")), "a_string", Value::Str(String::from("World!"))); let filter = Equals::from_args(args); - assert!(! filter.run(&mut record).unwrap()); + assert!(! filter.filter(&mut record)); let (args, mut record) = generate_args_record_custom("an_integer", Value::Int(2), "an_integer", Value::Int(3)); let filter = Equals::from_args(args); - assert!(! filter.run(&mut record).unwrap()); + assert!(! filter.filter(&mut record)); let (args, mut record) = generate_args_record_custom("a_date", Value::Date(Utc::now()), "a_date", Value::Date(Utc::now())); let filter = Equals::from_args(args); - assert!(! filter.run(&mut record).unwrap()); + assert!(! filter.filter(&mut record)); let (args, mut record) = generate_args_record_custom("first_one", Value::Int(1), "second_one", Value::Int(1)); let filter = Equals::from_args(args); - assert!(! filter.run(&mut record).unwrap()); + assert!(! filter.filter(&mut record)); } } diff --git a/src/modules.rs b/src/modules.rs index 7729708..2f80e64 100644 --- a/src/modules.rs +++ b/src/modules.rs @@ -1,23 +1,69 @@ -use crate::common::Record; -use crate::{actions,filters}; +use std::collections::HashMap; +use crate::common::{Record,Value}; -struct Available { +pub struct AvailableAction { name: &'static str, - cons: fn(ModuleArgs) -> Box + cons: fn(ModuleArgs) -> Box } -const AVAILABLE: &[Available] = &[ - Available { name: "action_noop", cons: move |a| Box::new(actions::Noop::from_args(a)) }, - Available { name: "filter_equals", cons: move |a| Box::new(filters::Equals::from_args(a)) } -]; - -pub trait Module { - fn run(&self, record: &mut Record) -> Result; +impl AvailableAction { + pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box) -> Self { + AvailableAction { name, cons } + } } -pub type ModuleArgs<'a> = Record<'a>; +inventory::collect!(AvailableAction); -pub enum ModuleType { - Filter, - Action +pub struct AvailableFilter { + name: &'static str, + cons: fn(ModuleArgs) -> Box } + +impl AvailableFilter { + pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box) -> Self { + AvailableFilter { name, cons } + } +} + +inventory::collect!(AvailableFilter); + +pub enum Module { + Action(Box), + Filter(Box) +} + +impl Module { + pub fn get_module(name: &str, args: ModuleArgs) -> Result { + for action in inventory::iter:: { + if action.name == name { + return Ok(Module::Action((action.cons)(args))) + } + } + for filter in inventory::iter:: { + if filter.name == name { + return Ok(Module::Filter((filter.cons)(args))) + } + } + Err(()) + } + + pub fn run(&self, record: &mut Record) -> Result { + match self { + Module::Action(a) => match a.act(record) { + Ok(()) => Ok(true), + Err(()) => Err(()) + }, + Module::Filter(f) => Ok(f.filter(record)) + } + } +} + +pub trait Action { + fn act(&self, record: &mut Record) -> Result<(), ()>; +} + +pub trait Filter { + fn filter(&self, record: &mut Record) -> bool; +} + +pub type ModuleArgs = HashMap;