diff --git a/docs/modeldescr/layout.rst b/docs/modeldescr/layout.rst index a6cdc722..f6b21b39 100644 --- a/docs/modeldescr/layout.rst +++ b/docs/modeldescr/layout.rst @@ -52,6 +52,7 @@ Model description index file has the following structure: This is a description of this model that gives you more idea what it is etc. maintainer: John Smith + checkbook: null config: null The following fields are supported: @@ -76,3 +77,23 @@ The following fields are supported: Global configuration section. It is applied to the whole session, globally. However different model can have a different configuration. + +``checkbook`` + + Checkbook is a list of sections that groups relations those needs to be checked. + An example: + + .. code-block:: yaml + + checkbook: + my_label: + - relation-one + - relation-two + + my_other_label: + - relation-one + - relation-three + + In this case user can call ``my_label`` and SysInspect will only go through relations, + grouped inside that section, leaving all other untouched. If checkbook is omitted, + then all relations will be examined, one after another. diff --git a/examples/models/router/actions/process-actions.cfg b/examples/models/router/actions/process-actions.cfg index bb1e79b7..81701497 100644 --- a/examples/models/router/actions/process-actions.cfg +++ b/examples/models/router/actions/process-actions.cfg @@ -33,7 +33,15 @@ actions: - udevd - journald state: + test: + args: + - foo: bar + - some: "other,stuff" $: + opts: + - one + - two + - "3" args: # Variable @(foo.bar) is contextual to the entity # and starts at current entity root. diff --git a/examples/models/router/model.cfg b/examples/models/router/model.cfg index 04332284..7ac22364 100644 --- a/examples/models/router/model.cfg +++ b/examples/models/router/model.cfg @@ -9,8 +9,18 @@ maintainer: John Smith # Check book is a list of relation IDs for evaluation and test cases # those needs to be performed in order to check the system checkbook: - - logging - - general-network + # A label of examination area + network: + - logging + - general-network + + system: + - systemd + - syslog # Global model configuration -config: null +config: + modules: /tmp/sysinspect/modules + foo: + - bar + - baz diff --git a/libsysinspect/src/intp/actions.rs b/libsysinspect/src/intp/actions.rs index 363bce89..f78dcd68 100644 --- a/libsysinspect/src/intp/actions.rs +++ b/libsysinspect/src/intp/actions.rs @@ -1,21 +1,50 @@ +use super::{actproc::modfinder::ModCall, inspector::SysInspector}; use crate::SysinspectError; +use colored::Colorize; use serde::{Deserialize, Serialize}; use serde_yaml::Value; -use std::collections::HashMap; +use std::{collections::HashMap, fmt::Display}; -#[derive(Debug, Serialize, Deserialize, Default)] +#[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct ModArgs { opts: Option>, args: Option>>, } -#[derive(Debug, Serialize, Deserialize, Default)] +impl ModArgs { + /// Get pairs of keyword args + pub fn args(&self) -> Vec<(String, String)> { + let mut out = Vec::<(String, String)>::default(); + if let Some(argset) = &self.args { + for kwargs in argset { + for (k, v) in kwargs { + out.push((k.to_owned(), v.to_owned())); + } + } + } + out + } + + /// Get options + pub fn opts(&self) -> Vec { + let mut out = Vec::::default(); + if let Some(optset) = &self.opts { + for opt in optset { + out.push(opt.to_owned()); + } + } + out + } +} + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] pub struct Action { id: Option, description: Option, module: String, bind: Vec, state: HashMap, + call: Option, } impl Action { @@ -37,8 +66,58 @@ impl Action { Ok(instance) } - /// Get action's `id` + /// Get action's `id` field pub fn id(&self) -> String { self.id.to_owned().unwrap_or("".to_string()) } + + /// Get action's `description` field + pub fn descr(&self) -> String { + self.description.to_owned().unwrap_or(format!("Action {}", self.id())) + } + + /// Returns true if an action has a bind to an entity via its `eid` _(entity Id)_. + pub fn binds_to(&self, eid: &str) -> bool { + self.bind.contains(&eid.to_string()) + } + + pub fn run(&self) { + if let Some(call) = &self.call { + log::debug!("Calling action {} on state {}", self.id().yellow(), call.state().yellow()); + call.run(); + } + } + + /// Setup and activate an action and is done by the Inspector. + /// This method finds module, sets up its parameters, binds constraint etc. + pub(crate) fn setup(&mut self, inspector: &SysInspector, state: String) -> Result { + let mpath = inspector.cfg().get_module(&self.module)?; + if let Some(mod_args) = self.state.get(&state) { + let mut modcall = ModCall::default().set_state(state).set_module(mpath); + for (kw, arg) in &mod_args.args() { + modcall.add_kwargs(kw.to_owned(), arg.to_owned()); + } + + for opt in &mod_args.opts() { + modcall.add_opt(opt.to_owned()); + } + self.call = Some(modcall); + } + Ok(self.to_owned()) + } +} + +impl Display for Action { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + " - Id: {}, Descr: {}, Module: {}, Active: {}", + self.id(), + self.descr(), + self.module, + self.call.is_some() + )?; + + Ok(()) + } } diff --git a/libsysinspect/src/intp/actproc/mod.rs b/libsysinspect/src/intp/actproc/mod.rs new file mode 100644 index 00000000..be2b0799 --- /dev/null +++ b/libsysinspect/src/intp/actproc/mod.rs @@ -0,0 +1 @@ +pub mod modfinder; diff --git a/libsysinspect/src/intp/actproc/modfinder.rs b/libsysinspect/src/intp/actproc/modfinder.rs new file mode 100644 index 00000000..2bfda77a --- /dev/null +++ b/libsysinspect/src/intp/actproc/modfinder.rs @@ -0,0 +1,62 @@ +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Display, path::PathBuf}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct ModCall { + state: String, + module: PathBuf, + args: Vec>, + opts: Vec, +} + +impl ModCall { + /// Set state + pub fn set_state(mut self, state: String) -> Self { + self.state = state; + self + } + + /// Set resolved module physical path + pub fn set_module(mut self, modpath: PathBuf) -> Self { + self.module = modpath; + self + } + + /// Add a pair of kwargs + pub fn add_kwargs(&mut self, kw: String, arg: String) -> &mut Self { + self.args.push([(kw, arg)].into_iter().collect()); + self + } + + /// Add an option + pub fn add_opt(&mut self, opt: String) -> &mut Self { + self.opts.push(opt); + self + } + + pub fn run(&self) { + log::debug!("run() of {}", self); + } + + pub fn state(&self) -> String { + self.state.to_owned() + } + + /// Get state ref + pub fn with_state(&self, state: String) -> bool { + self.state == state + } +} + +impl Default for ModCall { + fn default() -> Self { + Self { state: "$".to_string(), module: PathBuf::default(), args: Default::default(), opts: Default::default() } + } +} + +impl Display for ModCall { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ModCall - State: {}, Module: {:?}, Opts: {:?}, Args: {:?}", self.state, self.module, self.opts, self.args)?; + Ok(()) + } +} diff --git a/libsysinspect/src/intp/checkbook.rs b/libsysinspect/src/intp/checkbook.rs index 83970635..7d70a575 100644 --- a/libsysinspect/src/intp/checkbook.rs +++ b/libsysinspect/src/intp/checkbook.rs @@ -1,18 +1,66 @@ -use super::entities::Entity; -use std::collections::HashMap; +use super::relations::Relation; +use serde_yaml::Value; +use std::{collections::HashMap, fmt::Display}; -pub struct Checkbook { - entities: HashMap, +#[derive(Debug, Default)] +pub struct CheckbookSection { + id: String, + relations: Vec, } -impl Checkbook { +impl CheckbookSection { /// Initialise a checkbook. /// Entry is a list of relations needs to be examined. - pub fn new(entry: Vec) -> Self { - Checkbook { entities: HashMap::new() }.load(entry) + pub fn new(label: &Value, rel_ids: &Value, relations: &HashMap) -> Option { + let mut instance = CheckbookSection::default(); + + // No relations defined anyway + if relations.is_empty() { + return None; + } + + // Check if there is at least one requested Id in the set of relations + let mut orphans: Vec = Vec::default(); + if let Some(rel_ids) = rel_ids.as_sequence() { + for rid in rel_ids.iter().map(|s| s.as_str().unwrap_or("").to_string()).collect::>() { + if let Some(rel) = relations.get(&rid) { + instance.relations.push(rel.to_owned()); + } else { + orphans.push(rid); + } + } + instance.id = label.as_str().unwrap_or("").to_string(); + } + + // Checks + if instance.id.is_empty() { + log::error!("Checkbook section should have an Id"); + return None; + } + + // Feedback only + if !orphans.is_empty() { + log::warn!("Checkbook section \"{}\" has {} bogus relations: {}", instance.id, orphans.len(), orphans.join(", ")); + } + + // Discard invalid section + if instance.relations.is_empty() { + log::error!("Checkbook \"{}\" has no valid relations assotiated", instance.id); + return None; + } + + Some(instance) + } + + /// Get Id of the checkbook section + pub fn id(&self) -> String { + self.id.to_owned() } +} - fn load(self, entry: Vec) -> Self { - self +impl Display for CheckbookSection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "", self.id, self.relations)?; + Ok(()) } } diff --git a/libsysinspect/src/intp/conf.rs b/libsysinspect/src/intp/conf.rs new file mode 100644 index 00000000..88ff9eb2 --- /dev/null +++ b/libsysinspect/src/intp/conf.rs @@ -0,0 +1,40 @@ +use crate::SysinspectError; +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; +use std::path::PathBuf; + +#[derive(Debug, Serialize, Deserialize, Clone, Default)] +pub struct Config { + modules: PathBuf, +} + +impl Config { + pub fn new(obj: &Value) -> Result { + if let Ok(instance) = serde_yaml::from_value::(obj.to_owned()) { + return Ok(instance); + } + + Err(SysinspectError::ModelDSLError("Unable to parse configuration".to_string())) + } + + /// Get module from the namespace + pub fn get_module(&self, namespace: &str) -> Result { + // Fool-proof cleanup, likely a bad idea + let modpath = self.modules.join( + namespace + .trim_start_matches('.') + .trim_end_matches('.') + .trim() + .split('.') + .map(|s| s.to_string()) + .collect::>() + .join("/"), + ); + + if !modpath.exists() { + return Err(SysinspectError::ModuleError(format!("Module \"{}\" was not found", namespace))); + } + + Ok(modpath) + } +} diff --git a/libsysinspect/src/intp/inspector.rs b/libsysinspect/src/intp/inspector.rs index c8ca0b09..db8edcfc 100644 --- a/libsysinspect/src/intp/inspector.rs +++ b/libsysinspect/src/intp/inspector.rs @@ -1,6 +1,11 @@ -use super::{actions::Action, constraints::Constraint, entities::Entity, relations::Relation}; +use super::{ + actions::Action, checkbook::CheckbookSection, conf::Config, constraints::Constraint, entities::Entity, relations::Relation, +}; use crate::{ - mdl::{mspecdef::ModelSpec, DSL_DIR_ACTIONS, DSL_DIR_CONSTRAINTS, DSL_DIR_ENTITIES, DSL_DIR_RELATIONS}, + mdl::{ + mspecdef::ModelSpec, DSL_DIR_ACTIONS, DSL_DIR_CONSTRAINTS, DSL_DIR_ENTITIES, DSL_DIR_RELATIONS, DSL_IDX_CFG, + DSL_IDX_CHECKBOOK, + }, SysinspectError, }; use std::collections::HashMap; @@ -10,31 +15,39 @@ pub struct SysInspector { relations: HashMap, actions: HashMap, constraints: HashMap, + checkbook: Vec, + config: Config, + spec: ModelSpec, } impl SysInspector { - pub fn new(spec: &ModelSpec) -> Result { + pub fn new(spec: ModelSpec) -> Result { let mut sr = SysInspector { entities: HashMap::new(), relations: HashMap::new(), actions: HashMap::new(), constraints: HashMap::new(), + checkbook: Vec::default(), + config: Config::default(), + spec, }; - sr.load(spec)?; + sr.load()?; Ok(sr) } /// Load all objects. - fn load(&mut self, spec: &ModelSpec) -> Result<&mut Self, SysinspectError> { - for directive in [DSL_DIR_ENTITIES, DSL_DIR_ACTIONS, DSL_DIR_CONSTRAINTS, DSL_DIR_RELATIONS] { - let obj = spec.top(directive); - if !directive.eq(DSL_DIR_CONSTRAINTS) && obj.is_none() { + fn load(&mut self) -> Result<&mut Self, SysinspectError> { + for directive in + [DSL_DIR_ENTITIES, DSL_DIR_ACTIONS, DSL_DIR_CONSTRAINTS, DSL_DIR_RELATIONS, DSL_IDX_CHECKBOOK, DSL_IDX_CFG] + { + let v_obj = &self.spec.top(directive); + if !directive.eq(DSL_DIR_CONSTRAINTS) && v_obj.is_none() { return Err(SysinspectError::ModelDSLError(format!("Directive '{directive}' is not defined"))); } let mut amt = 0; - if let Some(obj) = obj.unwrap().as_mapping() { + if let Some(obj) = v_obj.unwrap().as_mapping() { for (obj_id, obj_data) in obj { match directive { d if d == DSL_DIR_ENTITIES => { @@ -57,9 +70,20 @@ impl SysInspector { self.relations.insert(obj.id(), obj); amt += 1; } + d if d == DSL_IDX_CHECKBOOK => { + if let Some(cs) = CheckbookSection::new(obj_id, obj_data, &self.relations) { + self.checkbook.push(cs); + amt += 1; + } + } _ => {} } } + + // Load config + if directive == DSL_IDX_CFG { + self.config = Config::new(v_obj.unwrap())?; + } } log::debug!("Loaded {amt} instances of {directive}"); @@ -67,4 +91,36 @@ impl SysInspector { Ok(self) } + + /// Get actions by relations + pub fn actions_by_relations(&self, rids: Vec) -> Result, SysinspectError> { + Ok(vec![]) + } + + /// Get actions by entities + pub fn actions_by_entities(&self, eids: Vec, state: Option) -> Result, SysinspectError> { + let mut out: Vec = Vec::default(); + let state = parse_state(state); + + for eid in eids { + for action in self.actions.values() { + if action.binds_to(&eid) { + log::debug!("Action entity: {}", action.id()); + out.push(action.to_owned().setup(self, state.to_owned())?); + } + } + } + + Ok(out) + } + + /// Return config reference + pub(crate) fn cfg(&self) -> &Config { + &self.config + } +} + +/// Parse state or return a default one +pub fn parse_state(state: Option) -> String { + state.unwrap_or("$".to_string()).trim().to_string() } diff --git a/libsysinspect/src/intp/mod.rs b/libsysinspect/src/intp/mod.rs index 0aa4da7c..091ae4a6 100644 --- a/libsysinspect/src/intp/mod.rs +++ b/libsysinspect/src/intp/mod.rs @@ -1,5 +1,7 @@ pub mod actions; +pub mod actproc; pub mod checkbook; +pub mod conf; pub mod constraints; pub mod entities; pub mod inspector; diff --git a/libsysinspect/src/intp/relations.rs b/libsysinspect/src/intp/relations.rs index b47d74c9..8e0f8641 100644 --- a/libsysinspect/src/intp/relations.rs +++ b/libsysinspect/src/intp/relations.rs @@ -1,10 +1,9 @@ -use std::collections::HashMap; - use crate::SysinspectError; use serde::{Deserialize, Serialize}; use serde_yaml::Value; +use std::collections::HashMap; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Relation { id: Option, #[serde(flatten)] diff --git a/libsysinspect/src/lib.rs b/libsysinspect/src/lib.rs index 2c65fc91..ac334ae1 100644 --- a/libsysinspect/src/lib.rs +++ b/libsysinspect/src/lib.rs @@ -17,6 +17,7 @@ pub enum SysinspectError { // Specific errors ModelMultipleIndex(String), ModelDSLError(String), + ModuleError(String), // Wrappers for the system errors IoErr(io::Error), @@ -41,6 +42,7 @@ impl Display for SysinspectError { SysinspectError::IoErr(err) => format!("(I/O) {err}"), SysinspectError::SerdeYaml(err) => format!("(YAML) {err}"), SysinspectError::ModelDSLError(err) => format!("(DSL) {err}"), + SysinspectError::ModuleError(err) => format!("(Module) {err}"), }; write!(f, "{msg}")?; diff --git a/libsysinspect/src/mdl/mod.rs b/libsysinspect/src/mdl/mod.rs index 0e6f690a..6366f5c8 100644 --- a/libsysinspect/src/mdl/mod.rs +++ b/libsysinspect/src/mdl/mod.rs @@ -6,3 +6,5 @@ pub static DSL_DIR_ENTITIES: &str = "entities"; pub static DSL_DIR_ACTIONS: &str = "actions"; pub static DSL_DIR_RELATIONS: &str = "relations"; pub static DSL_DIR_CONSTRAINTS: &str = "constraints"; +pub static DSL_IDX_CHECKBOOK: &str = "checkbook"; +pub static DSL_IDX_CFG: &str = "config"; diff --git a/scripts/temp_install_modules.sh b/scripts/temp_install_modules.sh new file mode 100755 index 00000000..5af02236 --- /dev/null +++ b/scripts/temp_install_modules.sh @@ -0,0 +1,12 @@ +#!/usr/bin/bash + +mroot="/tmp/sysinspect/modules" +mkdir -p $mroot + +# Sys +s_root="$mroot/sys" +mkdir -p $s_root + +for m in proc info; do + touch "$s_root/$m" +done diff --git a/src/clidef.rs b/src/clidef.rs index 8533f70e..74f3863c 100644 --- a/src/clidef.rs +++ b/src/clidef.rs @@ -1,4 +1,4 @@ -use clap::builder::styling; +use clap::{builder::styling, ArgMatches}; use clap::{Arg, ArgAction, Command}; use colored::Colorize; @@ -7,15 +7,15 @@ static APPNAME: &str = "sysinspect"; /// Define CLI arguments and styling pub fn cli(version: &'static str) -> Command { let styles = styling::Styles::styled() - .header(styling::AnsiColor::White.on_default() | styling::Effects::BOLD) - .usage(styling::AnsiColor::White.on_default() | styling::Effects::BOLD) - .literal(styling::AnsiColor::BrightCyan.on_default()) - .placeholder(styling::AnsiColor::Cyan.on_default()); + .header(styling::AnsiColor::Yellow.on_default()) + .usage(styling::AnsiColor::Yellow.on_default()) + .literal(styling::AnsiColor::BrightGreen.on_default()) + .placeholder(styling::AnsiColor::BrightMagenta.on_default()); Command::new(APPNAME) .version(version) - .about(format!("{} - {}", APPNAME, "is a tool for anomaly detection and root cause analysis in a known system")) - .override_usage(format!("{} {} {}", APPNAME.bright_cyan(), "[OPTIONS]".cyan(), "[FILTERS]".cyan())) + .about(format!("{} - {}", APPNAME.bright_magenta().bold(), "is a tool for anomaly detection and root cause analysis in a known system")) + .override_usage(format!("{} [OPTIONS] [FILTERS]", APPNAME)) // Config .arg( @@ -25,12 +25,27 @@ pub fn cli(version: &'static str) -> Command { .help("System description model") ) .arg( - Arg::new("relations") - .short('r') - .long("relations") - .help("Select only specific relations from the checkbook (comma-separated)") + Arg::new("labels") + .short('l') + .long("labels") + .help("Select only specific labels from the checkbook (comma-separated)") + .conflicts_with_all(["entities"]) + ) + .arg( + Arg::new("entities") + .short('e') + .long("entities") + .help("Select only specific entities from the inventory (comma-separated)") + .conflicts_with_all(["labels"]) + ) + .arg( + Arg::new("state") + .short('s') + .long("state") + .help("Specify a state to be processed. If none specified, default is taken ($)") ) + // Other .next_help_heading("Other") .arg( @@ -62,3 +77,20 @@ pub fn cli(version: &'static str) -> Command { If it doesn't work for you, please fill a bug report here: https://github.com/tinythings/sysinspect/issues\n".bright_yellow().to_string()) } + +/// Parse comma-separated values +pub fn split_by(am: &ArgMatches, id: &str, sep: Option) -> Vec { + let fsep: char; + if let Some(sep) = sep { + fsep = sep; + } else { + fsep = ','; + } + + am.get_one::(id) + .unwrap_or(&"".to_string()) + .split(fsep) + .map(|s| s.to_string()) + .filter(|s| !s.is_empty()) + .collect::>() +} diff --git a/src/main.rs b/src/main.rs index d22ffd93..016cb5e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,8 +45,24 @@ fn main() { match libsysinspect::mdl::mspec::load(mpath) { Ok(spec) => { log::debug!("Initalising inspector"); - match libsysinspect::intp::inspector::SysInspector::new(&spec) { - Ok(isp) => {} + match libsysinspect::intp::inspector::SysInspector::new(spec) { + Ok(isp) => { + // XXX: Move all this elsewhere + //let ar = isp.actions_by_relations(clidef::split_by(¶ms, "labels", None)).unwrap(); + match isp.actions_by_entities( + clidef::split_by(¶ms, "entities", None), + params.get_one::("state").cloned(), + ) { + Ok(actions) => { + for ac in actions { + ac.run(); + } + } + Err(err) => { + log::error!("{}", err); + } + } + } Err(err) => log::error!("{err}"), } log::debug!("Done");