diff --git a/Cargo.toml b/Cargo.toml index 8089823..0bc2b38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,9 @@ edition = "2018" [dependencies] chrono = "0.4" +indexmap = { version = "1.3", features = ["serde-1"] } inventory = "0.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" -systemd = "0.4" +systemd = "0.8" diff --git a/src/actions/mod.rs b/src/actions/mod.rs deleted file mode 100644 index ee1395c..0000000 --- a/src/actions/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -mod noop; -pub use self::noop::*; - -/* -pub trait Action { - fn act(&self, record: &mut Record) -> Result<(), ()>; -} - -impl Module for T { - fn run(&self, record: &mut Record) -> Result { - match self.act(record) { - Ok(()) => Ok(true), - Err(()) => Err(()) - } - } -} -*/ diff --git a/src/common.rs b/src/common.rs deleted file mode 100644 index cde950a..0000000 --- a/src/common.rs +++ /dev/null @@ -1,14 +0,0 @@ -use chrono::DateTime; -use std::collections::HashMap; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Value { - Bool(bool), - Str(String), - Int(isize), - Date(DateTime), - Map(HashMap), - List(Vec) -} - -pub type Record<'a> = HashMap<&'a str, Value>; diff --git a/src/config/mod.rs b/src/config/mod.rs deleted file mode 100644 index 3dc2449..0000000 --- a/src/config/mod.rs +++ /dev/null @@ -1,205 +0,0 @@ -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; - -mod file; - -thread_local!(static CONFIG: RefCell> = RefCell::new(None)); - -#[derive(Debug,Deserialize)] -pub struct Config { - actions: HashMap, - - #[serde(flatten)] - options: HashMap -} - -type Chain = Vec; - -#[derive(Debug,Deserialize)] -pub enum StepType { - #[serde(rename(deserialize = "action"))] - Action(String), - #[serde(rename(deserialize = "filter"))] - Filter(String) -} - -#[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/domain/action/mod.rs b/src/domain/action/mod.rs new file mode 100644 index 0000000..aac0a12 --- /dev/null +++ b/src/domain/action/mod.rs @@ -0,0 +1,2 @@ +mod noop; +pub use self::noop::*; diff --git a/src/actions/noop.rs b/src/domain/action/noop.rs similarity index 56% rename from src/actions/noop.rs rename to src/domain/action/noop.rs index 59d2522..9c17ffa 100644 --- a/src/actions/noop.rs +++ b/src/domain/action/noop.rs @@ -1,15 +1,10 @@ -use crate::modules::{Action,AvailableAction,ModuleArgs}; -use crate::common::Record; +use crate::domain::{Action, ModuleArgs, 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 { + pub fn from_args(_args: ModuleArgs) -> Noop { Noop {} } } @@ -22,12 +17,11 @@ impl Action for Noop { #[cfg(test)] mod tests { + use crate::domain::action::Noop; + use crate::domain::{Action, ModuleArgs, Record}; use std::collections::HashMap; - use crate::common::Record; - use crate::actions::Noop; - use crate::modules::{Action,ModuleArgs}; - fn generate_empty_args_record() -> (ModuleArgs, Record<'static>) { + fn generate_empty_args_record() -> (ModuleArgs, Record) { let args = HashMap::with_capacity(0); let record = HashMap::with_capacity(0); (args, record) @@ -35,8 +29,11 @@ mod tests { #[test] fn noop_does_nothing() { + // Given let (args, mut record) = generate_empty_args_record(); let action = Noop::from_args(args); + + // Then assert_eq!((), action.act(&mut record).unwrap()); } } diff --git a/src/domain/config.rs b/src/domain/config.rs new file mode 100644 index 0000000..562b4e6 --- /dev/null +++ b/src/domain/config.rs @@ -0,0 +1,21 @@ +use crate::domain::{ModuleArgs, Value}; +use indexmap::IndexMap; +use std::collections::HashMap; + +pub trait ConfigPort { + fn get(&self) -> &Config; +} + +pub struct Config { + pub actions: IndexMap, + pub options: HashMap, +} + +pub type Chain = Vec; + +pub struct Step { + pub module: String, + pub args: ModuleArgs, + pub then_dest: Option, + pub else_dest: Option, +} diff --git a/src/domain/filter/equals.rs b/src/domain/filter/equals.rs new file mode 100644 index 0000000..69a0169 --- /dev/null +++ b/src/domain/filter/equals.rs @@ -0,0 +1,179 @@ +use crate::domain::{Filter, ModuleArgs, Record, Value}; + +#[derive(Debug)] +pub struct Equals { + field: String, + value: Value, +} + +impl Equals { + pub fn from_args(mut args: ModuleArgs) -> Equals { + Equals { + field: match args.remove("field") { + Some(Value::Str(s)) => s, + _ => panic!("The Equals filter needs a field to filter in “field”"), + }, + value: args + .remove("value") + .expect("The Equals filter needs a reference value in “value”"), + } + } +} + +impl Filter for Equals { + fn filter(&self, record: &mut Record) -> bool { + match (record.get(&self.field), &self.value) { + (Some(ref v1), ref v2) => v1 == v2, + (None, _) => false, + } + } +} + +#[cfg(test)] +mod tests { + use crate::domain::filter::Equals; + use crate::domain::{Filter, ModuleArgs, Record, Value}; + use chrono::Utc; + use std::collections::HashMap; + + fn generate_args_record_equal(name: String, value: Value) -> (ModuleArgs, Record) { + let mut args = HashMap::with_capacity(2); + args.insert(String::from("field"), Value::Str(name.clone())); + 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( + ref_name: String, + ref_value: Value, + test_name: String, + test_value: Value, + ) -> (ModuleArgs, Record) { + let mut args = HashMap::with_capacity(2); + args.insert(String::from("field"), Value::Str(ref_name)); + args.insert(String::from("value"), ref_value); + let mut record = HashMap::with_capacity(1); + record.insert(test_name, test_value); + (args, record) + } + + #[test] + fn filter_equals_returns_true_for_identical_bools() { + // Given + let (args, mut record) = + generate_args_record_equal(String::from("a_boolean"), Value::Bool(false)); + let filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_true_for_identical_strings() { + // Given + let (args, mut record) = + generate_args_record_equal(String::from("a_string"), Value::Str(String::from("Hello!"))); + let filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_true_for_identical_ints() { + // Given + let (args, mut record) = generate_args_record_equal(String::from("an_integer"), Value::Int(2)); + let filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_true_for_identical_dates() { + // Given + let (args, mut record) = + generate_args_record_equal(String::from("a_date"), Value::Date(Utc::now())); + let filter = Equals::from_args(args); + + // Then + assert!(filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_bools() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("a_boolean"), + Value::Bool(true), + String::from("a_boolean"), + Value::Bool(false), + ); + let filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_strings() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("a_string"), + Value::Str(String::from("Hello!")), + String::from("a_string"), + Value::Str(String::from("World!")), + ); + let filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_ints() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("an_integer"), + Value::Int(2), + String::from("an_integer"), + Value::Int(3), + ); + let filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_different_dates() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("a_date"), + Value::Date(Utc::now()), + String::from("a_date"), + Value::Date(Utc::now()), + ); + let filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } + + #[test] + fn filter_equals_returns_false_for_non_matching_keys() { + // Given + let (args, mut record) = generate_args_record_custom( + String::from("first_one"), + Value::Int(1), + String::from("second_one"), + Value::Int(1), + ); + let filter = Equals::from_args(args); + + // Then + assert!(!filter.filter(&mut record)); + } +} diff --git a/src/domain/filter/mod.rs b/src/domain/filter/mod.rs new file mode 100644 index 0000000..e26dc52 --- /dev/null +++ b/src/domain/filter/mod.rs @@ -0,0 +1,2 @@ +mod equals; +pub use self::equals::*; diff --git a/src/domain/log.rs b/src/domain/log.rs new file mode 100644 index 0000000..18f8744 --- /dev/null +++ b/src/domain/log.rs @@ -0,0 +1,19 @@ +use crate::domain::Record; + +#[derive(Debug)] +pub enum LogMessage<'t> { + EMERG(&'t str), + ALERT(&'t str), + CRIT(&'t str), + ERR(&'t str), + WARNING(&'t str), + NOTICE(&'t str), + INFO(&'t str), + DEBUG(&'t str), +} + +pub trait LogPort: Sized { + fn open() -> Result; + fn read_next(&mut self) -> Result; + fn write(&mut self, message: LogMessage) -> Result<(), ()>; +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..ce85054 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,29 @@ +use chrono::DateTime; +use std::collections::HashMap; + +pub mod action; +pub mod filter; + +#[cfg(test)] +mod test_util; + +mod config; +pub use self::config::*; +mod log; +pub use self::log::*; +mod module; +pub use self::module::*; +mod workflow; +pub use self::workflow::*; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Value { + Bool(bool), + Str(String), + Int(isize), + Date(DateTime), + Map(HashMap), + List(Vec), +} + +pub type Record = HashMap; diff --git a/src/domain/module.rs b/src/domain/module.rs new file mode 100644 index 0000000..198a10c --- /dev/null +++ b/src/domain/module.rs @@ -0,0 +1,113 @@ +use crate::domain::{Record, Value}; +use std::collections::HashMap; +use std::fmt::Debug; + +pub struct AvailableAction { + pub name: String, + pub cons: fn(ModuleArgs) -> Box, +} + +impl AvailableAction { + pub fn new(name: String, cons: fn(ModuleArgs) -> Box) -> Self { + AvailableAction { name, cons } + } +} + +pub struct AvailableFilter { + pub name: String, + pub cons: fn(ModuleArgs) -> Box, +} + +impl AvailableFilter { + pub fn new(name: String, cons: fn(ModuleArgs) -> Box) -> Self { + AvailableFilter { name, cons } + } +} + +pub trait ModulesPort { + fn available_actions(&self) -> HashMap<&String, &AvailableAction>; + fn available_filters(&self) -> HashMap<&String, &AvailableFilter>; +} + +#[derive(Debug)] +pub enum Module { + Action(Box), + Filter(Box), +} + +impl Module { + pub fn new(name: String, args: ModuleArgs, available: &dyn ModulesPort) -> Result { + if let Some(a) = available.available_actions().get(&name).map(|m| m.cons) { + Ok(Module::Action(a(args))) + } else if let Some(f) = available.available_filters().get(&name).map(|m| m.cons) { + Ok(Module::Filter(f(args))) + } else { + 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: Debug { + fn act(&self, record: &mut Record) -> Result<(), ()>; +} + +pub trait Filter: Debug { + fn filter(&self, record: &mut Record) -> bool; +} + +pub type ModuleArgs = HashMap; + +#[cfg(test)] +mod tests { + use super::{AvailableAction, AvailableFilter, Module, Record, Value}; + use crate::domain::test_util::*; + use std::collections::HashMap; + + #[test] + fn available_action_can_be_generated_and_run() { + // Given + let aa = [AvailableAction { + name: ACT_NAME.to_string(), + cons: |_| Box::new(FakeAction {}), + }]; + let mut record: Record = HashMap::new(); + let mods = FakeModulesAdapter::new(&aa, &[]); + + // When + let module = Module::new(ACT_NAME.to_string(), HashMap::new(), &mods).unwrap(); + + // Then + assert!(module.run(&mut record) == Ok(true)); + assert!(record.contains_key(ACT_NAME)); + assert!(record[ACT_NAME] == Value::Int(1)); + } + + #[test] + fn available_filter_can_be_generated_and_run() { + // Given + let af = [AvailableFilter { + name: FLT_NAME.to_string(), + cons: |_| Box::new(FakeFilter {}), + }]; + let mut record: Record = HashMap::new(); + let mods = FakeModulesAdapter::new(&[], &af); + + // When + let module = Module::new(FLT_NAME.to_string(), HashMap::new(), &mods).unwrap(); + + // Then + assert!(module.run(&mut record) == Ok(false)); + assert!(record.contains_key(FLT_NAME)); + assert!(record[FLT_NAME] == Value::Int(1)); + } +} diff --git a/src/domain/test_util.rs b/src/domain/test_util.rs new file mode 100644 index 0000000..e2acec9 --- /dev/null +++ b/src/domain/test_util.rs @@ -0,0 +1,61 @@ +use crate::domain::{Action, AvailableAction, AvailableFilter, Filter, ModulesPort, Record, Value}; +use std::collections::HashMap; + +pub const ACT_NAME: &str = "fake_action"; +pub const FLT_NAME: &str = "fake_filter"; + +#[derive(Debug)] +pub struct FakeAction {} + +impl Action for FakeAction { + fn act(&self, record: &mut Record) -> Result<(), ()> { + let v = record.get(ACT_NAME).unwrap_or(&Value::Int(0)); + match v { + Value::Int(i) => record.insert(String::from(ACT_NAME), Value::Int(i + 1)), + _ => panic!("The record did not contain the expected value."), + }; + Ok(()) + } +} + +#[derive(Debug)] +pub struct FakeFilter {} + +impl Filter for FakeFilter { + fn filter(&self, record: &mut Record) -> bool { + let v = record.get(FLT_NAME).unwrap_or(&Value::Int(0)); + match v { + Value::Int(i) => record.insert(String::from(FLT_NAME), Value::Int(i + 1)), + _ => panic!("The record did not contain the expected value."), + }; + false + } +} + +pub struct FakeModulesAdapter<'a> { + a: HashMap<&'a String, &'a AvailableAction>, + f: HashMap<&'a String, &'a AvailableFilter>, +} + +impl FakeModulesAdapter<'_> { + pub fn new<'a>(act: &'a [AvailableAction], flt: &'a [AvailableFilter]) -> FakeModulesAdapter<'a> { + let a = act + .iter() + .map(|m| (&m.name, m)) + .collect::>(); + let f = flt + .iter() + .map(|m| (&m.name, m)) + .collect::>(); + FakeModulesAdapter { a, f } + } +} + +impl ModulesPort for FakeModulesAdapter<'_> { + fn available_actions(&self) -> HashMap<&String, &AvailableAction> { + self.a.clone() + } + fn available_filters(&self) -> HashMap<&String, &AvailableFilter> { + self.f.clone() + } +} diff --git a/src/domain/workflow.rs b/src/domain/workflow.rs new file mode 100644 index 0000000..639c09d --- /dev/null +++ b/src/domain/workflow.rs @@ -0,0 +1,328 @@ +use crate::domain::{Config, Module, ModulesPort, Record, Step}; +use std::collections::HashMap; +use std::ops::Add; + +#[derive(Debug)] +struct Node { + name: String, + module: Module, + then_dest: isize, // <0 for a leaf-Node + else_dest: isize, // <0 for a leaf-Node +} + +impl Node { + fn run(&self, record: &mut Record) -> isize { + if let Ok(b) = self.module.run(record) { + if b { + self.then_dest + } else { + self.else_dest + } + } else { + -1 + } + } +} + +pub struct Workflow { + nodes: Vec, +} + +impl Workflow { + pub fn run(&self, record: &mut Record) { + let mut i = 0 as isize; + while { + i = self.nodes[i as usize].run(record); + i >= 0 + } {} + } + + pub fn build(conf: &mut Config, available: &dyn ModulesPort) -> Workflow { + let mut seen: Vec = Vec::new(); + let mut nodes: Vec = Vec::new(); + let mut dangling: DanglingInfo = HashMap::new(); + for (name, chain) in conf.actions.drain(..) { + build_chain(name, chain, &mut nodes, &mut seen, &mut dangling, available); + } + if nodes.is_empty() { + panic!("A configuration must have at least one module."); + } + if !dangling.is_empty() { + let mut error = "Incomplete configuration:".to_string(); + let mut unknown = false; + for (o, v) in dangling { + if let Some(c) = o { + unknown = true; + error = format!( + "{}\n\tReference to unknown chain “{}” found at:", + &error, &c + ); + for (i, is_then) in v { + error = format!( + "{}\n\t {}:{}", + &error, + nodes[i].name, + if is_then { "then" } else { "else" } + ); + } + } + } + if unknown { + panic!(error); + } + } + Workflow { nodes } + } +} + +type DanglingInfo = HashMap, Vec<(usize, bool)>>; + +fn build_chain( + chain_name: String, + chain: Vec, + nodes: &mut Vec, + seen: &mut Vec, + dangling: &mut DanglingInfo, + available: &dyn ModulesPort, +) { + let mut index = 0; + for step in chain { + let next_node_u = nodes.len(); + let next_node_i = next_node_u as isize; + let mut then_was_used = false; + if index == 0 { + if let Some(v) = dangling.remove(&Some(&chain_name).cloned()) { + for (i, is_then) in v { + match is_then { + true => nodes.get_mut(i).unwrap().then_dest = next_node_i, + false => nodes.get_mut(i).unwrap().else_dest = next_node_i, + }; + } + } else if let Some(v) = dangling.remove(&None) { + for (i, is_then) in v { + match is_then { + true => nodes.get_mut(i).unwrap().then_dest = next_node_i, + false => nodes.get_mut(i).unwrap().else_dest = next_node_i, + }; + } + } + } else { + nodes.get_mut(next_node_u - 1).unwrap().then_dest = next_node_i; + } + let name = chain_name + .clone() + .add(&format!("[{}]:{}", index, &step.module)); + let module = Module::new(step.module, step.args, available) + .expect(&format!("Module {} could not be created.", &name)); + let then_dest = step + .then_dest + .map(|c| { + if seen.contains(&c) { + panic!(format!("Configuration loop at {}:then", name)); + } + then_was_used = true; + node_wants_chain(next_node_u, true, dangling, Some(c)); + -1 + }) + .unwrap_or(-1); + let else_dest = step + .else_dest + .map(|c| { + if seen.contains(&c) { + panic!(format!("Configuration loop at {}:else", name)); + } + node_wants_chain(next_node_u, false, dangling, Some(c)); + -1 + }) + .unwrap_or({ + node_wants_chain(next_node_u, false, dangling, None); + -1 + }); + nodes.push(Node { + name, + module, + then_dest, + else_dest, + }); + index += 1; + if then_was_used { + break; + } + } +} + +fn node_wants_chain( + next_node_index: usize, + is_then: bool, + dangling: &mut DanglingInfo, + wanted_chain: Option, +) { + let mut d = dangling.remove(&wanted_chain).unwrap_or(Vec::new()); + d.push((next_node_index, is_then)); + dangling.insert(wanted_chain, d); +} + +#[cfg(test)] +mod tests { + use crate::domain::test_util::*; + use crate::domain::{AvailableAction, AvailableFilter, Chain, Config, Record, Step, Value, Workflow}; + use indexmap::IndexMap; + use std::collections::HashMap; + + #[test] + #[should_panic(expected = "A configuration must have at least one module.")] + fn empty_config_results_in_panic() { + // Given + let mut conf = Config { + actions: IndexMap::new(), + options: HashMap::new(), + }; + let mods = FakeModulesAdapter::new(&[], &[]); + + // When + let _wf = Workflow::build(&mut conf, &mods); + } + + #[test] + fn config_with_one_module_is_ok() { + // Given + let mut actions: IndexMap = IndexMap::new(); + actions.insert( + "chain1".to_string(), + vec![Step { + module: ACT_NAME.to_string(), + args: HashMap::new(), + then_dest: None, + else_dest: None, + }], + ); + let mut conf = Config { + actions, + options: HashMap::new(), + }; + let aa = [AvailableAction { + name: ACT_NAME.to_string(), + cons: |_| Box::new(FakeAction {}), + }]; + let mods = FakeModulesAdapter::new(&aa, &[]); + let mut record: Record = HashMap::new(); + + // When + let wf = Workflow::build(&mut conf, &mods); + wf.run(&mut record); + + // Then + assert!(wf.nodes.len() == 1); + assert!(wf.nodes[0].name == "chain1[0]:fake_action"); + assert!(wf.nodes[0].then_dest < 0); + assert!(wf.nodes[0].else_dest < 0); + assert!(record[ACT_NAME] == Value::Int(1)); + assert!(record.get(FLT_NAME) == None); + } + + #[test] + fn explicit_chain_to_chain_link_works() { + // Given + let mut actions: IndexMap = IndexMap::new(); + actions.insert( + "chain1".to_string(), + vec![Step { + module: FLT_NAME.to_string(), + args: HashMap::new(), + then_dest: None, + else_dest: Some("chain2".to_string()), + }], + ); + actions.insert( + "chain2".to_string(), + vec![Step { + module: ACT_NAME.to_string(), + args: HashMap::new(), + then_dest: None, + else_dest: None, + }], + ); + let mut conf = Config { + actions, + options: HashMap::new(), + }; + let aa = [AvailableAction { + name: ACT_NAME.to_string(), + cons: |_| Box::new(FakeAction {}), + }]; + let af = [AvailableFilter { + name: FLT_NAME.to_string(), + cons: |_| Box::new(FakeFilter {}), + }]; + let mods = FakeModulesAdapter::new(&aa, &af); + let mut record: Record = HashMap::new(); + + // When + let wf = Workflow::build(&mut conf, &mods); + wf.run(&mut record); + + // Then + assert!(wf.nodes.len() == 2); + assert!(wf.nodes[0].name == "chain1[0]:fake_filter"); + assert!(wf.nodes[1].name == "chain2[0]:fake_action"); + assert!(wf.nodes[0].then_dest < 0); + assert!(wf.nodes[0].else_dest == 1); + assert!(wf.nodes[1].then_dest < 0); + assert!(wf.nodes[1].else_dest < 0); + assert!(record[ACT_NAME] == Value::Int(1)); + assert!(record[FLT_NAME] == Value::Int(1)); + } + + #[test] + fn implicit_chain_to_chain_link_works() { + // Given + let mut actions: IndexMap = IndexMap::new(); + actions.insert( + "chain1".to_string(), + vec![Step { + module: FLT_NAME.to_string(), + args: HashMap::new(), + then_dest: None, + else_dest: None, + }], + ); + actions.insert( + "chain2".to_string(), + vec![Step { + module: ACT_NAME.to_string(), + args: HashMap::new(), + then_dest: None, + else_dest: None, + }], + ); + let mut conf = Config { + actions, + options: HashMap::new(), + }; + let aa = [AvailableAction { + name: ACT_NAME.to_string(), + cons: |_| Box::new(FakeAction {}), + }]; + let af = [AvailableFilter { + name: FLT_NAME.to_string(), + cons: |_| Box::new(FakeFilter {}), + }]; + let mods = FakeModulesAdapter::new(&aa, &af); + let mut record: Record = HashMap::new(); + + // When + let wf = Workflow::build(&mut conf, &mods); + wf.run(&mut record); + + // Then + assert!(wf.nodes.len() == 2); + assert!(wf.nodes[0].name == "chain1[0]:fake_filter"); + assert!(wf.nodes[1].name == "chain2[0]:fake_action"); + assert!(wf.nodes[0].then_dest < 0); + assert!(wf.nodes[0].else_dest == 1); + assert!(wf.nodes[1].then_dest < 0); + assert!(wf.nodes[1].else_dest < 0); + assert!(record[ACT_NAME] == Value::Int(1)); + assert!(record[FLT_NAME] == Value::Int(1)); + } +} diff --git a/src/filters/equals.rs b/src/filters/equals.rs deleted file mode 100644 index 748abe8..0000000 --- a/src/filters/equals.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crate::modules::{AvailableFilter,Filter,ModuleArgs}; -use crate::common::Record; -use crate::common::Value; - -#[derive(Debug)] -pub struct Equals { - field: String, - 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 { - field: match args.remove("field") { - Some(Value::Str(s)) => s, - _ => panic!("The Equals filter needs a field to filter in “field”") - }, - value: args.remove("value").expect("The Equals filter needs a reference value in “value”") - } - } -} - -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) => v1 == v2, - (None, _) => false - } - } -} - -#[cfg(test)] -mod tests { - use chrono::Utc; - use std::collections::HashMap; - use crate::common::{Record,Value}; - use crate::filters::Equals; - use crate::modules::{Filter,ModuleArgs}; - - fn generate_args_record_equal<'a>(name: &'a str, value: Value) -> (ModuleArgs, Record<'a>) { - let mut args = HashMap::with_capacity(2); - 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, Record<'a>) { - let mut args = HashMap::with_capacity(2); - 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) - } - - #[test] - 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.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.filter(&mut record)); - - let (args, mut record) = generate_args_record_equal("an_integer", Value::Int(2)); - let filter = Equals::from_args(args); - 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.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.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.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.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.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.filter(&mut record)); - } -} diff --git a/src/filters/mod.rs b/src/filters/mod.rs deleted file mode 100644 index d05eaff..0000000 --- a/src/filters/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod equals; -pub use self::equals::*; - -/* -pub trait Filter { - fn filter(&self, record: &mut Record) -> bool; -} - -impl Module for T { - fn run(&self, record: &mut Record) -> Result { - Ok(self.filter(record)) - } -} -*/ diff --git a/src/infra/action.rs b/src/infra/action.rs new file mode 100644 index 0000000..de3c2fe --- /dev/null +++ b/src/infra/action.rs @@ -0,0 +1,28 @@ +use crate::domain::action::Noop; +use crate::domain::AvailableAction; + +const ACTION_NOOP: &str = "action_noop"; + +inventory::submit! { + AvailableAction::new(ACTION_NOOP.to_string(), move |a| Box::new(Noop::from_args(a))) +} + +#[cfg(test)] +mod tests { + use crate::domain::{ModuleArgs, ModulesPort}; + use crate::infra::module::InventoryModulesAdapter; + use std::collections::HashMap; + + #[test] + fn action_noop_is_available() { + // Given + let args: ModuleArgs = HashMap::new(); + + // When + let aa = (InventoryModulesAdapter {}).available_actions(); + + // Then + assert!(aa.contains_key(&super::ACTION_NOOP.to_string())); + let _can_instantiate = (aa[&super::ACTION_NOOP.to_string()].cons)(args); + } +} diff --git a/src/config/file.rs b/src/infra/config/file.rs similarity index 50% rename from src/config/file.rs rename to src/infra/config/file.rs index f790655..dffd1f3 100644 --- a/src/config/file.rs +++ b/src/infra/config/file.rs @@ -1,21 +1,33 @@ +use super::SerdeConfigAdapter; use std::env; -use std::ffi::{OsString}; -use std::io::{BufReader,Error,Result}; -use std::path::Path; +use std::ffi::OsString; use std::fs::File; +use std::io::BufReader; +use std::path::Path; const ENV_VARIABLE: &'static str = "PYRUSE_CONF"; +const ETC_PATH: &'static str = "/etc/pyruse"; -enum ConfFile { +pub enum ConfFile { Json(OsString), - Yaml(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"))) - }; +impl ConfFile { + pub fn from_filesystem() -> ConfFile { + find_file(find_candidates()) + } + + pub fn to_config(self) -> SerdeConfigAdapter { + match self { + ConfFile::Json(path) => { + SerdeConfigAdapter::from_json(BufReader::new(File::open(path).expect("Read error"))) + } + ConfFile::Yaml(path) => { + SerdeConfigAdapter::from_yaml(BufReader::new(File::open(path).expect("Read error"))) + } + } + } } fn find_candidates() -> Vec { @@ -29,14 +41,17 @@ fn find_candidates() -> Vec { 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()) + _ => 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")) + ConfFile::Json(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.json"))), + ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yaml"))), + ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yml"))), ] } } diff --git a/src/infra/config/mod.rs b/src/infra/config/mod.rs new file mode 100644 index 0000000..9a0747f --- /dev/null +++ b/src/infra/config/mod.rs @@ -0,0 +1,336 @@ +use crate::domain::{Chain, Config, ConfigPort, ModuleArgs, Step, Value}; +use indexmap::IndexMap; +use serde::de::{self, Deserializer, MapAccess, SeqAccess, Visitor}; +use serde::Deserialize; +use serde_json; +use serde_yaml; +use std::collections::HashMap; +use std::fmt; +use std::io::Read; + +mod file; + +struct SerdeConfigAdapter { + config: Config, +} + +impl SerdeConfigAdapter { + pub fn from_json(data: impl Read) -> SerdeConfigAdapter { + let serde_config: SerdeConfig = + serde_json::from_reader(data).expect("Failed to parse configuration"); + SerdeConfigAdapter::from_serde(serde_config) + } + fn from_yaml(data: impl Read) -> SerdeConfigAdapter { + let serde_config: SerdeConfig = + serde_yaml::from_reader(data).expect("Failed to parse configuration"); + SerdeConfigAdapter::from_serde(serde_config) + } + + fn from_serde(mut data: SerdeConfig) -> SerdeConfigAdapter { + SerdeConfigAdapter { + config: Config { + actions: data + .actions + .drain(..) + .map(|(name, mut serde_chain)| { + ( + name, + serde_chain + .drain(..) + .map(|step| Step { + module: match step.module { + StepType::Action(s) => s, + StepType::Filter(s) => s, + }, + args: step.args, + then_dest: step.then_dest, + else_dest: step.else_dest, + }) + .collect::(), + ) + }) + .collect::>(), + options: data.options, + }, + } + } +} + +impl ConfigPort for SerdeConfigAdapter { + fn get(&self) -> &Config { + &self.config + } +} + +#[derive(Debug, Deserialize)] +pub struct SerdeConfig { + actions: IndexMap, + + #[serde(flatten)] + options: HashMap, +} + +type SerdeChain = Vec; + +#[derive(Debug, Deserialize, Eq, PartialEq)] +pub enum StepType { + #[serde(rename(deserialize = "action"))] + Action(String), + #[serde(rename(deserialize = "filter"))] + Filter(String), +} + +#[derive(Debug, Deserialize)] +pub struct SerdeStep { + #[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) + } +} + +/* *** end of serde *** */ + +#[cfg(test)] +mod tests { + use super::SerdeConfigAdapter; + use crate::domain::Value; + + #[test] + fn parse_json_works() { + // Given + 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" } + } + ], + "… Report insufficient buffer-size for Nextcloud QUERY_STRING": [ + ] + }, + "debug": false + } + "#.as_bytes(); + + // When + let conf = SerdeConfigAdapter::from_json(json).config; + + // Then + assert!(conf.actions.len() == 2); + assert!(conf.actions.get_index(0).unwrap().0 == "Detect request errors with Nextcloud"); + assert!( + conf.actions.get_index(1).unwrap().0 + == "… Report insufficient buffer-size for Nextcloud QUERY_STRING" + ); + assert!(conf.actions.get_index(0).unwrap().1.len() == 3); + assert!(conf.actions.get_index(0).unwrap().1[1].module == String::from("filter_pcre")); + assert!(conf.options.get("debug") == Some(&Value::Bool(false))); + } +} diff --git a/src/infra/filter.rs b/src/infra/filter.rs new file mode 100644 index 0000000..4da2be9 --- /dev/null +++ b/src/infra/filter.rs @@ -0,0 +1,30 @@ +use crate::domain::filter::Equals; +use crate::domain::AvailableFilter; + +const FILTER_EQUALS: &str = "filter_equals"; + +inventory::submit! { + AvailableFilter::new(FILTER_EQUALS.to_string(), move |a| Box::new(Equals::from_args(a))) +} + +#[cfg(test)] +mod tests { + use crate::domain::{ModuleArgs, ModulesPort, Value}; + use crate::infra::module::InventoryModulesAdapter; + use std::collections::HashMap; + + #[test] + fn filter_equals_is_available() { + // Given + let mut args: ModuleArgs = HashMap::new(); + args.insert("field".to_string(), Value::Str("a_field".to_string())); + args.insert("value".to_string(), Value::Int(1)); + + // When + let af = (InventoryModulesAdapter {}).available_filters(); + + // Then + assert!(af.contains_key(&super::FILTER_EQUALS.to_string())); + let _can_instantiate = (af[&super::FILTER_EQUALS.to_string()].cons)(args); + } +} diff --git a/src/infra/log.rs b/src/infra/log.rs new file mode 100644 index 0000000..aa51840 --- /dev/null +++ b/src/infra/log.rs @@ -0,0 +1,155 @@ +use crate::domain::{LogMessage, LogPort, Record, Value}; +use chrono::DateTime; +use std::collections::HashMap; +use std::iter::FromIterator; +use systemd::journal::{print, Journal, OpenOptions}; + +type JournalFieldMapper = fn(String) -> Value; + +const STR_MAPPER: JournalFieldMapper = |s| Value::Str(s); +const INT_MAPPER: JournalFieldMapper = |s| { + s.parse::() + .map(|i| Value::Int(i)) + .unwrap_or(Value::Str(s)) +}; +const DATE_MAPPER: JournalFieldMapper = |s| { + s.parse::>() + .map(|d| Value::Date(d)) + .unwrap_or(Value::Str(s)) +}; + +pub struct SystemdLogAdapter { + journal: Journal, + mappers: HashMap, +} + +impl LogPort for SystemdLogAdapter { + fn open() -> Result { + let mappers = create_mappers(); + if let Ok(mut journal) = OpenOptions::default() + .system(true) + .current_user(false) + .local_only(false) + .open() + { + if let Ok(_) = journal.seek_tail() { + return Ok(SystemdLogAdapter { journal, mappers }); + } + } + Err(()) + } + + fn read_next(&mut self) -> Result { + loop { + match self.journal.await_next_entry(None) { + Err(_) => return Err(()), + Ok(Some(mut entry)) => { + let mut record: Record = HashMap::with_capacity(entry.len()); + let all_keys = entry.keys().map(|s| s.clone()).collect::>(); + for k in all_keys { + let (k, v) = entry.remove_entry(&k).unwrap(); + let mapper = self.mappers.get(&k); + if mapper == None { + continue; + } + record.insert(k, (mapper.unwrap())(v)); + } + return Ok(record); + } + Ok(None) => continue, + }; + } + } + + fn write(&mut self, message: LogMessage) -> Result<(), ()> { + let unix_status = match message { + LogMessage::EMERG(m) => print(0, m), + LogMessage::ALERT(m) => print(1, m), + LogMessage::CRIT(m) => print(2, m), + LogMessage::ERR(m) => print(3, m), + LogMessage::WARNING(m) => print(4, m), + LogMessage::NOTICE(m) => print(5, m), + LogMessage::INFO(m) => print(6, m), + LogMessage::DEBUG(m) => print(7, m), + }; + match unix_status { + 0 => Ok(()), + _ => Err(()), + } + } +} + +fn create_mappers<'t>() -> HashMap { + let map: HashMap = HashMap::from_iter( + [ + ("MESSAGE", STR_MAPPER), + ("MESSAGE_ID", STR_MAPPER), + ("PRIORITY", INT_MAPPER), + ("CODE_FILE", STR_MAPPER), + ("CODE_LINE", INT_MAPPER), + ("CODE_FUNC", STR_MAPPER), + ("ERRNO", INT_MAPPER), + ("INVOCATION_ID", STR_MAPPER), + ("USER_INVOCATION_ID", STR_MAPPER), + ("SYSLOG_FACILITY", INT_MAPPER), + ("SYSLOG_IDENTIFIER", STR_MAPPER), + ("SYSLOG_PID", INT_MAPPER), + ("SYSLOG_TIMESTAMP", DATE_MAPPER), + ("SYSLOG_RAW", STR_MAPPER), + ("DOCUMENTATION", STR_MAPPER), + ("TID", INT_MAPPER), + ("_PID", INT_MAPPER), + ("_UID", INT_MAPPER), + ("_GID", INT_MAPPER), + ("_COMM", STR_MAPPER), + ("_EXE", STR_MAPPER), + ("_CMDLINE", STR_MAPPER), + ("_CAP_EFFECTIVE", STR_MAPPER), + ("_AUDIT_SESSION", STR_MAPPER), + ("_AUDIT_LOGINUID", INT_MAPPER), + ("_SYSTEMD_CGROUP", STR_MAPPER), + ("_SYSTEMD_SLICE", STR_MAPPER), + ("_SYSTEMD_UNIT", STR_MAPPER), + ("_SYSTEMD_USER_UNIT", STR_MAPPER), + ("_SYSTEMD_USER_SLICE", STR_MAPPER), + ("_SYSTEMD_SESSION", STR_MAPPER), + ("_SYSTEMD_OWNER_UID", INT_MAPPER), + ("_SELINUX_CONTEXT", STR_MAPPER), + ("_SOURCE_REALTIME_TIMESTAMP", DATE_MAPPER), + ("_BOOT_ID", STR_MAPPER), + ("_MACHINE_ID", STR_MAPPER), + ("_SYSTEMD_INVOCATION_ID", STR_MAPPER), + ("_HOSTNAME", STR_MAPPER), + ("_TRANSPORT", STR_MAPPER), + ("_STREAM_ID", STR_MAPPER), + ("_LINE_BREAK", STR_MAPPER), + ("_NAMESPACE", STR_MAPPER), + ("_KERNEL_DEVICE", STR_MAPPER), + ("_KERNEL_SUBSYSTEM", STR_MAPPER), + ("_UDEV_SYSNAME", STR_MAPPER), + ("_UDEV_DEVNODE", STR_MAPPER), + ("_UDEV_DEVLINK", STR_MAPPER), + ("COREDUMP_UNIT", STR_MAPPER), + ("COREDUMP_USER_UNIT", STR_MAPPER), + ("OBJECT_PID", INT_MAPPER), + ("OBJECT_UID", INT_MAPPER), + ("OBJECT_GID", INT_MAPPER), + ("OBJECT_COMM", STR_MAPPER), + ("OBJECT_EXE", STR_MAPPER), + ("OBJECT_CMDLINE", STR_MAPPER), + ("OBJECT_AUDIT_SESSION", STR_MAPPER), + ("OBJECT_AUDIT_LOGINUID", INT_MAPPER), + ("OBJECT_SYSTEMD_CGROUP", STR_MAPPER), + ("OBJECT_SYSTEMD_SESSION", STR_MAPPER), + ("OBJECT_SYSTEMD_OWNER_UID", INT_MAPPER), + ("OBJECT_SYSTEMD_UNIT", STR_MAPPER), + ("OBJECT_SYSTEMD_USER_UNIT", STR_MAPPER), + ("__CURSOR", STR_MAPPER), + ("__REALTIME_TIMESTAMP", DATE_MAPPER), + ("__MONOTONIC_TIMESTAMP", DATE_MAPPER), + ] + .iter() + .map(|(s, m)| (s.to_string(), m.to_owned())), + ); + map +} diff --git a/src/infra/mod.rs b/src/infra/mod.rs new file mode 100644 index 0000000..7b01b94 --- /dev/null +++ b/src/infra/mod.rs @@ -0,0 +1,5 @@ +pub mod action; +pub mod config; +pub mod filter; +pub mod log; +pub mod module; diff --git a/src/infra/module.rs b/src/infra/module.rs new file mode 100644 index 0000000..aa73000 --- /dev/null +++ b/src/infra/module.rs @@ -0,0 +1,25 @@ +use crate::domain::{AvailableAction, AvailableFilter, ModulesPort}; +use std::collections::HashMap; + +inventory::collect!(AvailableAction); +inventory::collect!(AvailableFilter); + +pub struct InventoryModulesAdapter {} + +impl ModulesPort for InventoryModulesAdapter { + fn available_actions(&self) -> HashMap<&String, &AvailableAction> { + let mut h: HashMap<&String, &AvailableAction> = HashMap::new(); + for action in inventory::iter:: { + h.insert(&action.name, &action); + } + h + } + + fn available_filters(&self) -> HashMap<&String, &AvailableFilter> { + let mut h: HashMap<&String, &AvailableFilter> = HashMap::new(); + for filter in inventory::iter:: { + h.insert(&filter.name, &filter); + } + h + } +} diff --git a/src/main.rs b/src/main.rs index dc86bba..ffd51aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,6 @@ -mod actions; -mod common; -mod config; -mod filters; -mod modules; +mod domain; +mod service; +mod infra; fn main() { println!("Hello, world!"); diff --git a/src/modules.rs b/src/modules.rs deleted file mode 100644 index 2f80e64..0000000 --- a/src/modules.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::collections::HashMap; -use crate::common::{Record,Value}; - -pub struct AvailableAction { - name: &'static str, - cons: fn(ModuleArgs) -> Box -} - -impl AvailableAction { - pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box) -> Self { - AvailableAction { name, cons } - } -} - -inventory::collect!(AvailableAction); - -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; diff --git a/src/service/mod.rs b/src/service/mod.rs new file mode 100644 index 0000000..e69de29