clean architecture + log, workflow
parent
d441ed3b14
commit
5e120a05bb
|
@ -8,8 +8,9 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
indexmap = { version = "1.3", features = ["serde-1"] }
|
||||||
inventory = "0.1"
|
inventory = "0.1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.8"
|
serde_yaml = "0.8"
|
||||||
systemd = "0.4"
|
systemd = "0.8"
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
mod noop;
|
|
||||||
pub use self::noop::*;
|
|
||||||
|
|
||||||
/*
|
|
||||||
pub trait Action {
|
|
||||||
fn act(&self, record: &mut Record) -> Result<(), ()>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Action> Module for T {
|
|
||||||
fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
|
||||||
match self.act(record) {
|
|
||||||
Ok(()) => Ok(true),
|
|
||||||
Err(()) => Err(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -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<chrono::Utc>),
|
|
||||||
Map(HashMap<String, Value>),
|
|
||||||
List(Vec<Value>)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Record<'a> = HashMap<&'a str, Value>;
|
|
|
@ -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<Option<Config>> = RefCell::new(None));
|
|
||||||
|
|
||||||
#[derive(Debug,Deserialize)]
|
|
||||||
pub struct Config {
|
|
||||||
actions: HashMap<String, Chain>,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
options: HashMap<String, Value>
|
|
||||||
}
|
|
||||||
|
|
||||||
type Chain = Vec<Step>;
|
|
||||||
|
|
||||||
#[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<String>,
|
|
||||||
#[serde(rename(deserialize = "else"))]
|
|
||||||
else_dest: Option<String>
|
|
||||||
}
|
|
||||||
|
|
||||||
/* *** 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<E>(self, v: bool) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Bool(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Int(v as isize))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_char<E>(self, v: char) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Str(v.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Str(String::from(v)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Str(String::from(v)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_string<E>(self, v: String) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Str(v))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E> 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<E>(self, v: &'de [u8]) -> Result<Self::Value, E> 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<E>(self, v: Vec<u8>) -> Result<Self::Value, E> where E: de::Error {
|
|
||||||
Ok(Value::Str(String::from_utf8(v).expect("Strings in the configuration must be UTF-8")))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> 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<A>(self, mut map: A) -> Result<Self::Value, A::Error> 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<D>(deserializer: D) -> Result<Value, D::Error> 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());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod noop;
|
||||||
|
pub use self::noop::*;
|
|
@ -1,15 +1,10 @@
|
||||||
use crate::modules::{Action,AvailableAction,ModuleArgs};
|
use crate::domain::{Action, ModuleArgs, Record};
|
||||||
use crate::common::Record;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Noop {}
|
pub struct Noop {}
|
||||||
|
|
||||||
inventory::submit! {
|
|
||||||
AvailableAction::new("action_noop", move |a| Box::new(Noop::from_args(a)))
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Noop {
|
impl Noop {
|
||||||
pub fn from_args(mut _args: ModuleArgs) -> Noop {
|
pub fn from_args(_args: ModuleArgs) -> Noop {
|
||||||
Noop {}
|
Noop {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,12 +17,11 @@ impl Action for Noop {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use crate::domain::action::Noop;
|
||||||
|
use crate::domain::{Action, ModuleArgs, Record};
|
||||||
use std::collections::HashMap;
|
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 args = HashMap::with_capacity(0);
|
||||||
let record = HashMap::with_capacity(0);
|
let record = HashMap::with_capacity(0);
|
||||||
(args, record)
|
(args, record)
|
||||||
|
@ -35,8 +29,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn noop_does_nothing() {
|
fn noop_does_nothing() {
|
||||||
|
// Given
|
||||||
let (args, mut record) = generate_empty_args_record();
|
let (args, mut record) = generate_empty_args_record();
|
||||||
let action = Noop::from_args(args);
|
let action = Noop::from_args(args);
|
||||||
|
|
||||||
|
// Then
|
||||||
assert_eq!((), action.act(&mut record).unwrap());
|
assert_eq!((), action.act(&mut record).unwrap());
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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<String, Chain>,
|
||||||
|
pub options: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Chain = Vec<Step>;
|
||||||
|
|
||||||
|
pub struct Step {
|
||||||
|
pub module: String,
|
||||||
|
pub args: ModuleArgs,
|
||||||
|
pub then_dest: Option<String>,
|
||||||
|
pub else_dest: Option<String>,
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
mod equals;
|
||||||
|
pub use self::equals::*;
|
|
@ -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<Self, ()>;
|
||||||
|
fn read_next(&mut self) -> Result<Record, ()>;
|
||||||
|
fn write(&mut self, message: LogMessage) -> Result<(), ()>;
|
||||||
|
}
|
|
@ -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<chrono::Utc>),
|
||||||
|
Map(HashMap<String, Value>),
|
||||||
|
List(Vec<Value>),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Record = HashMap<String, Value>;
|
|
@ -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<dyn Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvailableAction {
|
||||||
|
pub fn new(name: String, cons: fn(ModuleArgs) -> Box<dyn Action>) -> Self {
|
||||||
|
AvailableAction { name, cons }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AvailableFilter {
|
||||||
|
pub name: String,
|
||||||
|
pub cons: fn(ModuleArgs) -> Box<dyn Filter>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AvailableFilter {
|
||||||
|
pub fn new(name: String, cons: fn(ModuleArgs) -> Box<dyn Filter>) -> 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<dyn Action>),
|
||||||
|
Filter(Box<dyn Filter>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Module {
|
||||||
|
pub fn new(name: String, args: ModuleArgs, available: &dyn ModulesPort) -> Result<Module, ()> {
|
||||||
|
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<bool, ()> {
|
||||||
|
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<String, Value>;
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::<HashMap<&'a String, &'a AvailableAction>>();
|
||||||
|
let f = flt
|
||||||
|
.iter()
|
||||||
|
.map(|m| (&m.name, m))
|
||||||
|
.collect::<HashMap<&'a String, &'a AvailableFilter>>();
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Node>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String> = Vec::new();
|
||||||
|
let mut nodes: Vec<Node> = 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<Option<String>, Vec<(usize, bool)>>;
|
||||||
|
|
||||||
|
fn build_chain(
|
||||||
|
chain_name: String,
|
||||||
|
chain: Vec<Step>,
|
||||||
|
nodes: &mut Vec<Node>,
|
||||||
|
seen: &mut Vec<String>,
|
||||||
|
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<String>,
|
||||||
|
) {
|
||||||
|
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<String, Chain> = 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<String, Chain> = 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<String, Chain> = 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
mod equals;
|
|
||||||
pub use self::equals::*;
|
|
||||||
|
|
||||||
/*
|
|
||||||
pub trait Filter {
|
|
||||||
fn filter(&self, record: &mut Record) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Filter> Module for T {
|
|
||||||
fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
|
||||||
Ok(self.filter(record))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,33 @@
|
||||||
|
use super::SerdeConfigAdapter;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::{OsString};
|
use std::ffi::OsString;
|
||||||
use std::io::{BufReader,Error,Result};
|
|
||||||
use std::path::Path;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
|
use std::io::BufReader;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
const ENV_VARIABLE: &'static str = "PYRUSE_CONF";
|
const ENV_VARIABLE: &'static str = "PYRUSE_CONF";
|
||||||
|
const ETC_PATH: &'static str = "/etc/pyruse";
|
||||||
|
|
||||||
enum ConfFile {
|
pub enum ConfFile {
|
||||||
Json(OsString),
|
Json(OsString),
|
||||||
Yaml(OsString)
|
Yaml(OsString),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_file() {
|
impl ConfFile {
|
||||||
match find_file(find_candidates()) {
|
pub fn from_filesystem() -> ConfFile {
|
||||||
ConfFile::Json(path) => super::parse_json(BufReader::new(File::open(path).expect("Read error"))),
|
find_file(find_candidates())
|
||||||
ConfFile::Yaml(path) => super::parse_yaml(BufReader::new(File::open(path).expect("Read error")))
|
}
|
||||||
};
|
|
||||||
|
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<ConfFile> {
|
fn find_candidates() -> Vec<ConfFile> {
|
||||||
|
@ -29,14 +41,17 @@ fn find_candidates() -> Vec<ConfFile> {
|
||||||
match s.as_ref() {
|
match s.as_ref() {
|
||||||
"json" => vec![ConfFile::Json(path)],
|
"json" => vec![ConfFile::Json(path)],
|
||||||
"yaml" | "yml" => vec![ConfFile::Yaml(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 => {
|
None => {
|
||||||
vec![
|
vec![
|
||||||
ConfFile::Json(OsString::from("pyruse.json")),
|
ConfFile::Json(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.json"))),
|
||||||
ConfFile::Yaml(OsString::from("pyruse.yaml")),
|
ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yaml"))),
|
||||||
ConfFile::Yaml(OsString::from("pyruse.yml"))
|
ConfFile::Yaml(OsString::from(format!("{}/{}", ETC_PATH, "pyruse.yml"))),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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::<Chain>(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect::<IndexMap<String, Chain>>(),
|
||||||
|
options: data.options,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConfigPort for SerdeConfigAdapter {
|
||||||
|
fn get(&self) -> &Config {
|
||||||
|
&self.config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct SerdeConfig {
|
||||||
|
actions: IndexMap<String, SerdeChain>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
options: HashMap<String, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type SerdeChain = Vec<SerdeStep>;
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
#[serde(rename(deserialize = "else"))]
|
||||||
|
else_dest: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/* *** 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<E>(self, v: bool) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Bool(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i8<E>(self, v: i8) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i16<E>(self, v: i16) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i32<E>(self, v: i32) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i64<E>(self, v: i64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_i128<E>(self, v: i128) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u8<E>(self, v: u8) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_u128<E>(self, v: u128) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_f32<E>(self, v: f32) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_f64<E>(self, v: f64) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Int(v as isize))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_char<E>(self, v: char) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Str(v.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Str(String::from(v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Str(String::from(v)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Str(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
|
||||||
|
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<E>(self, v: &'de [u8]) -> Result<Self::Value, E>
|
||||||
|
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<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
|
||||||
|
where
|
||||||
|
E: de::Error,
|
||||||
|
{
|
||||||
|
Ok(Value::Str(
|
||||||
|
String::from_utf8(v).expect("Strings in the configuration must be UTF-8"),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
|
||||||
|
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<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||||
|
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<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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::<isize>()
|
||||||
|
.map(|i| Value::Int(i))
|
||||||
|
.unwrap_or(Value::Str(s))
|
||||||
|
};
|
||||||
|
const DATE_MAPPER: JournalFieldMapper = |s| {
|
||||||
|
s.parse::<DateTime<chrono::Utc>>()
|
||||||
|
.map(|d| Value::Date(d))
|
||||||
|
.unwrap_or(Value::Str(s))
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SystemdLogAdapter {
|
||||||
|
journal: Journal,
|
||||||
|
mappers: HashMap<String, JournalFieldMapper>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LogPort for SystemdLogAdapter {
|
||||||
|
fn open() -> Result<Self, ()> {
|
||||||
|
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<Record, ()> {
|
||||||
|
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::<Vec<String>>();
|
||||||
|
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<String, JournalFieldMapper> {
|
||||||
|
let map: HashMap<String, JournalFieldMapper> = 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
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod action;
|
||||||
|
pub mod config;
|
||||||
|
pub mod filter;
|
||||||
|
pub mod log;
|
||||||
|
pub mod module;
|
|
@ -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::<AvailableAction> {
|
||||||
|
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::<AvailableFilter> {
|
||||||
|
h.insert(&filter.name, &filter);
|
||||||
|
}
|
||||||
|
h
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
mod actions;
|
mod domain;
|
||||||
mod common;
|
mod service;
|
||||||
mod config;
|
mod infra;
|
||||||
mod filters;
|
|
||||||
mod modules;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Hello, world!");
|
println!("Hello, world!");
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use crate::common::{Record,Value};
|
|
||||||
|
|
||||||
pub struct AvailableAction {
|
|
||||||
name: &'static str,
|
|
||||||
cons: fn(ModuleArgs) -> Box<dyn Action>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AvailableAction {
|
|
||||||
pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box<dyn Action>) -> Self {
|
|
||||||
AvailableAction { name, cons }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inventory::collect!(AvailableAction);
|
|
||||||
|
|
||||||
pub struct AvailableFilter {
|
|
||||||
name: &'static str,
|
|
||||||
cons: fn(ModuleArgs) -> Box<dyn Filter>
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AvailableFilter {
|
|
||||||
pub fn new(name: &'static str, cons: fn(ModuleArgs) -> Box<dyn Filter>) -> Self {
|
|
||||||
AvailableFilter { name, cons }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inventory::collect!(AvailableFilter);
|
|
||||||
|
|
||||||
pub enum Module {
|
|
||||||
Action(Box<dyn Action>),
|
|
||||||
Filter(Box<dyn Filter>)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Module {
|
|
||||||
pub fn get_module(name: &str, args: ModuleArgs) -> Result<Module, ()> {
|
|
||||||
for action in inventory::iter::<AvailableAction> {
|
|
||||||
if action.name == name {
|
|
||||||
return Ok(Module::Action((action.cons)(args)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for filter in inventory::iter::<AvailableFilter> {
|
|
||||||
if filter.name == name {
|
|
||||||
return Ok(Module::Filter((filter.cons)(args)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(&self, record: &mut Record) -> Result<bool, ()> {
|
|
||||||
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<String, Value>;
|
|
Loading…
Reference in New Issue