clean architecture + log, workflow

rust-rewrite
Yves G 2021-02-20 18:53:11 +01:00
parent d441ed3b14
commit 5e120a05bb
26 changed files with 1376 additions and 454 deletions

View File

@ -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"

View File

@ -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(())
}
}
}
*/

View File

@ -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>;

View File

@ -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());
});
}
}

2
src/domain/action/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod noop;
pub use self::noop::*;

View File

@ -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());
}
}

21
src/domain/config.rs Normal file
View File

@ -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>,
}

179
src/domain/filter/equals.rs Normal file
View File

@ -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));
}
}

2
src/domain/filter/mod.rs Normal file
View File

@ -0,0 +1,2 @@
mod equals;
pub use self::equals::*;

19
src/domain/log.rs Normal file
View File

@ -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<(), ()>;
}

29
src/domain/mod.rs Normal file
View File

@ -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>;

113
src/domain/module.rs Normal file
View File

@ -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));
}
}

61
src/domain/test_util.rs Normal file
View File

@ -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()
}
}

328
src/domain/workflow.rs Normal file
View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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))
}
}
*/

28
src/infra/action.rs Normal file
View File

@ -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);
}
}

View File

@ -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<ConfFile> {
@ -29,14 +41,17 @@ fn find_candidates() -> Vec<ConfFile> {
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"))),
]
}
}

336
src/infra/config/mod.rs Normal file
View File

@ -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)));
}
}

30
src/infra/filter.rs Normal file
View File

@ -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);
}
}

155
src/infra/log.rs Normal file
View File

@ -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
}

5
src/infra/mod.rs Normal file
View File

@ -0,0 +1,5 @@
pub mod action;
pub mod config;
pub mod filter;
pub mod log;
pub mod module;

25
src/infra/module.rs Normal file
View File

@ -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
}
}

View File

@ -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!");

View File

@ -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>;

0
src/service/mod.rs Normal file
View File