wip(generic-snmp): with a better json structure. Not functional

This commit is contained in:
David Boucher 2025-03-27 15:20:39 +01:00
parent 19a0a89634
commit 968922478e
9 changed files with 481 additions and 344 deletions

View File

@ -18,7 +18,7 @@ rasn = "0.24.0"
rasn-snmp = "0.24.0"
rasn-smi = "0.24.0"
log = "0.4.25"
clap = { version = "4.5.29", features = ["derive"] }
lexopt = "0.3.0"
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
lalrpop-util = { version = "0.22.1", features = ["lexer"] }

View File

@ -4,7 +4,7 @@
{
"name": "cpu",
"oid": "1.3.6.1.2.1.25.3.3.1.2",
"query": "Walk", // par défaut walk
"query": "Walk"
}
]
},
@ -13,18 +13,18 @@
{
"name": "core.cpu.usage.percent",
"value": "{cpu}",
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
"uom": "%",
"min": 0,
"max": 100
}
],
"aggregations": [
{
"name": "avg.cpu.usage.percent",
"value": "Average({cpu})", // si agregation, pas de prefix
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
"value": "Average({cpu})",
"uom": "%",
"min": 0,
"max": 100
}
]
}

View File

@ -0,0 +1,31 @@
{
"collect": {
"snmp": [
{
"name": "cpu",
"oid": "1.3.6.1.2.1.25.3.3.1.2",
"query": "Walk", // par défaut walk
}
]
},
"compute": {
"metrics": [
{
"name": "core.cpu.usage.percent",
"value": "{cpu}",
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
}
],
"aggregations": [
{
"name": "avg.cpu.usage.percent",
"value": "Average({cpu})", // si agregation, pas de prefix
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
}
]
}
}

View File

@ -4,7 +4,7 @@
{
"name": "disk",
"oid": "1.3.6.1.2.1.25.2.3.1",
"query": "Walk", // par défaut walk
"query": "Walk",
"labels": {
".3": "label",
".4": "allocationUnits",
@ -17,28 +17,28 @@
"compute": {
"metrics": [
{
"prefix": "{disk.label}", // par défaut "{idx}"
"prefix": "{disk.label}",
"name": "disk.usage.percent",
"value": "100 * {disk.used} / {disk.size}",
"uom": "%", // optional
"min": 0, // optional
"max": 100, // optional
"uom": "%",
"min": 0,
"max": 100
},
{
"name": "disk.usage.bytes",
"value": "{disk.used} * {disk.allocationUnits}",
"uom": "B", // optional
"min": 0, // optional
"max_expr": "{disk.size} * {disk.allocationUnits}" // optional
"uom": "B",
"min": 0,
"max_expr": "{disk.size} * {disk.allocationUnits}"
}
],
"aggregations": [
{
"name": "avg.disk.usage.percent",
"value": "Average({metrics.disk.usage.percent})", // si agregation, pas de prefix
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
"value": "Average({metrics.disk.usage.percent})",
"uom": "%",
"min": 0,
"max": 100
}
]
}

View File

@ -0,0 +1,46 @@
{
"collect": {
"snmp": [
{
"name": "disk",
"oid": "1.3.6.1.2.1.25.2.3.1",
"query": "Walk", // par défaut walk
"labels": {
".3": "label",
".4": "allocationUnits",
".5": "size",
".6": "used"
}
}
]
},
"compute": {
"metrics": [
{
"prefix": "{disk.label}", // par défaut "{idx}"
"name": "disk.usage.percent",
"value": "100 * {disk.used} / {disk.size}",
"uom": "%", // optional
"min": 0, // optional
"max": 100, // optional
},
{
"name": "disk.usage.bytes",
"value": "{disk.used} * {disk.allocationUnits}",
"uom": "B", // optional
"min": 0, // optional
"max_expr": "{disk.size} * {disk.allocationUnits}" // optional
}
],
"aggregations": [
{
"name": "avg.disk.usage.percent",
"value": "Average({metrics.disk.usage.percent})", // si agregation, pas de prefix
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
}
]
}
}

View File

@ -4,12 +4,12 @@
{
"name": "total",
"oid": "1.3.6.1.4.1.2021.4.6.0",
"query": "Get", // par défaut walk
"query": "Get"
},
{
"name": "free",
"oid": "1.3.6.1.4.1.2021.4.11.0",
"query": "Get", // par défaut walk
"query": "Get"
}
]
},
@ -18,16 +18,17 @@
{
"name": "core.mem.usage.percent",
"value": "100 * (1 - {free}/{cpu})",
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
"uom": "%",
"min": 0,
"max": 100,
"threshold-suffix": "mem"
},
{
"name": "avg.cpu.usage.percent",
"value": "Average({cpu})", // si agregation, pas de prefix
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
"value": "Average({cpu})",
"uom": "%",
"min": 0,
"max": 100
}
]
}

View File

@ -0,0 +1,36 @@
{
"collect": {
"snmp": [
{
"name": "total",
"oid": "1.3.6.1.4.1.2021.4.6.0",
"query": "Get", // par défaut walk
},
{
"name": "free",
"oid": "1.3.6.1.4.1.2021.4.11.0",
"query": "Get", // par défaut walk
}
]
},
"compute": {
"metrics": [
{
"name": "core.mem.usage.percent",
"value": "100 * (1 - {free}/{cpu})",
"uom": "%", // optional
"min": 0, // optional
"max": 100, // optional
"threshold-suffix": "mem", // optional (example: --warning-mem=78 --critical-mem=90)
},
{
"name": "avg.cpu.usage.percent",
"value": "Average({cpu})", // si agregation, pas de prefix
"uom": "%", // optional
"min": 0, // optional
"max": 100 // optional
}
]
}
}

View File

@ -23,15 +23,6 @@ impl Status {
}
}
struct Metric<'b> {
name: String,
value: f32,
warning: &'b Option<String>,
critical: &'b Option<String>,
status: Status,
agregated: bool,
}
fn worst(a: Status, b: Status) -> Status {
let a_int = match a {
Status::Ok => 0,
@ -52,60 +43,42 @@ fn worst(a: Status, b: Status) -> Status {
}
}
#[derive(Deserialize, Debug, Clone, Copy)]
enum Operation {
None,
Average,
}
#[derive(Deserialize, Debug)]
enum QueryType {
Walk,
}
#[derive(Deserialize, Debug)]
struct EntryOperation {
struct Metric {
name: String,
op: Operation,
value: String,
uom: Option<String>,
min: Option<f32>,
max: Option<f32>,
}
#[derive(Deserialize, Debug)]
struct EntryQuery {
pub struct Compute {
metrics: Vec<Metric>,
aggregations: Vec<Metric>,
}
#[derive(Deserialize, Debug)]
pub struct Snmp {
name: String,
oid: String,
query: QueryType,
}
#[derive(Deserialize, Debug)]
enum Entry {
Agregation(EntryOperation),
Query(EntryQuery),
}
#[derive(Deserialize, Debug)]
struct Data {
uom: String,
min: Option<f32>,
max: Option<f32>,
}
#[derive(Deserialize, Debug)]
struct OutputTable {
header: String,
text: Vec<String>,
}
#[derive(Deserialize, Debug)]
struct Leaf {
name: String,
output: OutputTable,
entries: Vec<Entry>,
data: Option<Data>,
pub struct Collect {
snmp: Vec<Snmp>,
}
#[derive(Deserialize, Debug)]
pub struct Command {
leaf: Leaf,
collect: Collect,
compute: Compute,
}
pub struct CmdResult {
@ -136,66 +109,66 @@ fn compute_status(value: f32, warn: &Option<String>, crit: &Option<String>) -> S
Status::Ok
}
fn build_metrics<'a>(
values: &Vec<(String, f32)>,
ag: &Option<(&str, usize, f32)>,
ext: &'a CommandExt,
) -> (Vec<Metric<'a>>, Status) {
let mut metrics: Vec<Metric> = Vec::new();
let mut status = Status::Ok;
match ag {
Some(a) => {
// The agregation is located in first place
if a.1 == 0 {
let w = &ext.warning_agregation;
let c = &ext.critical_agregation;
let current_status =
compute_status(a.2, &ext.warning_agregation, &ext.critical_agregation);
metrics.push(Metric {
name: a.0.to_string(),
value: a.2,
warning: w,
critical: c,
status: current_status,
agregated: true,
});
status = worst(current_status, status);
}
}
None => (),
}
values.iter().enumerate().for_each(|(i, v)| {
let current_status = compute_status(v.1, &ext.warning_core, &ext.critical_core);
metrics.push(Metric {
name: values[i].0.clone(),
value: v.1,
warning: &ext.warning_core,
critical: &ext.critical_core,
status: current_status,
agregated: false,
});
status = worst(current_status, status);
});
match ag {
Some(a) => {
if a.1 > 0 {
let current_status =
compute_status(a.2, &ext.warning_agregation, &ext.critical_agregation);
metrics.push(Metric {
name: a.0.to_string(),
value: a.2,
warning: &ext.warning_agregation,
critical: &ext.critical_agregation,
status: current_status,
agregated: true,
});
status = worst(current_status, status);
}
}
None => (),
}
(metrics, status)
}
//fn build_metrics<'a>(
// values: &Vec<(String, f32)>,
// ag: &Option<(&str, usize, f32)>,
// ext: &'a CommandExt,
//) -> (Vec<Metric<'a>>, Status) {
// let mut metrics: Vec<Metric> = Vec::new();
// let mut status = Status::Ok;
// match ag {
// Some(a) => {
// // The agregation is located in first place
// if a.1 == 0 {
// let w = &ext.warning_agregation;
// let c = &ext.critical_agregation;
// let current_status =
// compute_status(a.2, &ext.warning_agregation, &ext.critical_agregation);
// metrics.push(Metric {
// name: a.0.to_string(),
// value: a.2,
// warning: w,
// critical: c,
// status: current_status,
// agregated: true,
// });
// status = worst(current_status, status);
// }
// }
// None => (),
// }
// values.iter().enumerate().for_each(|(i, v)| {
// let current_status = compute_status(v.1, &ext.warning_core, &ext.critical_core);
// metrics.push(Metric {
// name: values[i].0.clone(),
// value: v.1,
// warning: &ext.warning_core,
// critical: &ext.critical_core,
// status: current_status,
// agregated: false,
// });
// status = worst(current_status, status);
// });
// match ag {
// Some(a) => {
// if a.1 > 0 {
// let current_status =
// compute_status(a.2, &ext.warning_agregation, &ext.critical_agregation);
// metrics.push(Metric {
// name: a.0.to_string(),
// value: a.2,
// warning: &ext.warning_agregation,
// critical: &ext.critical_agregation,
// status: current_status,
// agregated: true,
// });
// status = worst(current_status, status);
// }
// }
// None => (),
// }
// (metrics, status)
//}
impl Command {
pub fn execute(
@ -205,193 +178,197 @@ impl Command {
community: &str,
ext: &CommandExt,
) -> CmdResult {
let mut agregation = ("", 0, Operation::None);
let mut res: Option<(&str, SnmpResult)> = None;
for (idx, entry) in self.leaf.entries.iter().enumerate() {
match entry {
Entry::Agregation(op) => {
agregation = (&op.name, idx, op.op);
}
Entry::Query(query) => match query.query {
QueryType::Walk => {
res = Some((
&query.name,
r_snmp_bulk_walk(target, version, community, &query.oid),
));
}
},
}
}
match res {
Some(r) => {
let mut values: Vec<(String, f32)> = Vec::new();
let mut idx = 0;
r.1.variables.iter().for_each(|v| {
let label = r.0.replace("{idx}", &idx.to_string());
values.push((label, v.value.parse().unwrap()));
idx += 1;
});
let count = values.len();
let ag = match agregation.2 {
Operation::Average => {
let sum: f32 = values.iter().map(|(_, v)| v).sum();
let avg = sum / values.len() as f32;
Some((agregation.0, agregation.1, avg))
}
_ => None,
};
let (metrics, status) = build_metrics(&values, &ag, &ext);
let output = self.build_output(count, status, &metrics, &ag, &ext);
return CmdResult { status, output };
}
None => {
return CmdResult {
status: Status::Unknown,
output: "No result".to_string(),
};
}
// let mut agregation = ("", 0, Operation::None);
// let mut res: Option<(&str, SnmpResult)> = None;
// for (idx, entry) in self.leaf.entries.iter().enumerate() {
// match entry {
// Entry::Agregation(op) => {
// agregation = (&op.name, idx, op.op);
// }
// Entry::Query(query) => match query.query {
// QueryType::Walk => {
// res = Some((
// &query.name,
// r_snmp_bulk_walk(target, version, community, &query.oid),
// ));
// }
// },
// }
// }
// match res {
// Some(r) => {
// let mut values: Vec<(String, f32)> = Vec::new();
// let mut idx = 0;
// r.1.variables.iter().for_each(|v| {
// let label = r.0.replace("{idx}", &idx.to_string());
// values.push((label, v.value.parse().unwrap()));
// idx += 1;
// });
// let count = values.len();
// let ag = match agregation.2 {
// Operation::Average => {
// let sum: f32 = values.iter().map(|(_, v)| v).sum();
// let avg = sum / values.len() as f32;
// Some((agregation.0, agregation.1, avg))
// }
// _ => None,
// };
// let (metrics, status) = build_metrics(&values, &ag, &ext);
// let output = self.build_output(count, status, &metrics, &ag, &ext);
// return CmdResult { status, output };
// }
// None => {
// return CmdResult {
// status: Status::Unknown,
// output: "No result".to_string(),
// };
// }
// }
CmdResult {
status: Status::Unknown,
output: "No result".to_string(),
}
}
fn build_output(
&self,
count: usize,
status: Status,
metrics: &Vec<Metric>,
ag: &Option<(&str, usize, f32)>,
ext: &CommandExt,
) -> String {
let no_threshold = ext.warning_core.is_none()
&& ext.critical_core.is_none()
&& ext.warning_agregation.is_none()
&& ext.critical_agregation.is_none();
let write_details =
no_threshold || (ext.warning_core.is_some() || ext.critical_core.is_some());
let write_agregation_details =
no_threshold || (ext.warning_agregation.is_some() || ext.critical_agregation.is_some());
let mut output_text = "".to_string();
let mut begun = false;
if &self.leaf.output.header != "" {
output_text = self.leaf.output.header.replace("{status}", status.as_str());
}
for line in &self.leaf.output.text {
if line.contains("idx") {
if write_details {
// We have to iterate on metrics
let mut output_vec = (Vec::new(), Vec::new(), Vec::new());
let mut idx = 0;
for m in metrics.iter() {
if !m.agregated {
let text = line
.replace("{idx}", idx.to_string().as_str())
.replace("{name}", m.name.as_str())
.replace("{value}", format!("{:.2}", m.value).as_str())
.replace("{count}", count.to_string().as_str());
match m.status {
Status::Ok => {
output_vec.0.push(text);
}
Status::Warning => {
output_vec.1.push(text);
}
Status::Critical => {
output_vec.2.push(text);
}
Status::Unknown => (),
}
idx += 1;
}
}
if !output_vec.2.is_empty() {
if begun {
output_text += " - ";
} else {
begun = true;
}
output_text += output_vec.2.join(" - ").as_str();
}
if !output_vec.1.is_empty() {
if begun {
output_text += " - ";
} else {
begun = true;
}
output_text += output_vec.1.join(" - ").as_str();
}
if !output_vec.0.is_empty() {
if begun {
output_text += " - ";
}
output_text += output_vec.0.join(" - ").as_str();
}
}
} else {
if write_agregation_details {
match ag {
Some(a) => {
output_text += line
.replace(
format!("{{{}}}", a.0).as_str(),
format!("{:.2}", a.2).as_str(),
)
.replace("{count}", count.to_string().as_str())
.as_str();
begun = true;
}
None => output_text += line,
};
}
}
}
let mut perfdata = " |".to_string();
match &self.leaf.data {
Some(d) => {
metrics.iter().for_each(|m| {
perfdata += format!(
" '{}'={}{};{};{};{};{}",
m.name,
m.value,
d.uom,
match m.warning {
Some(m) => m.to_string(),
None => "".to_string(),
},
match m.critical {
Some(m) => m.to_string(),
None => "".to_string(),
},
match d.min {
Some(m) => m.to_string(),
None => "".to_string(),
},
match d.max {
Some(m) => m.to_string(),
None => "".to_string(),
},
)
.as_str();
});
}
None => {
metrics.iter().for_each(|m| {
perfdata += format!(
" '{}'={};{};{}",
m.name,
m.value,
match m.warning {
Some(v) => v.to_string(),
None => "".to_string(),
},
match m.critical {
Some(v) => v.to_string(),
None => "".to_string(),
}
)
.as_str();
});
}
};
output_text + &perfdata
}
// fn build_output(
// &self,
// count: usize,
// status: Status,
// metrics: &Vec<Metric>,
// ag: &Option<(&str, usize, f32)>,
// ext: &CommandExt,
// ) -> String {
// let no_threshold = ext.warning_core.is_none()
// && ext.critical_core.is_none()
// && ext.warning_agregation.is_none()
// && ext.critical_agregation.is_none();
// let write_details =
// no_threshold || (ext.warning_core.is_some() || ext.critical_core.is_some());
// let write_agregation_details =
// no_threshold || (ext.warning_agregation.is_some() || ext.critical_agregation.is_some());
// let mut output_text = "".to_string();
// let mut begun = false;
// if &self.leaf.output.header != "" {
// output_text = self.leaf.output.header.replace("{status}", status.as_str());
// }
// for line in &self.leaf.output.text {
// if line.contains("idx") {
// if write_details {
// // We have to iterate on metrics
// let mut output_vec = (Vec::new(), Vec::new(), Vec::new());
// let mut idx = 0;
// for m in metrics.iter() {
// if !m.agregated {
// let text = line
// .replace("{idx}", idx.to_string().as_str())
// .replace("{name}", m.name.as_str())
// .replace("{value}", format!("{:.2}", m.value).as_str())
// .replace("{count}", count.to_string().as_str());
// match m.status {
// Status::Ok => {
// output_vec.0.push(text);
// }
// Status::Warning => {
// output_vec.1.push(text);
// }
// Status::Critical => {
// output_vec.2.push(text);
// }
// Status::Unknown => (),
// }
// idx += 1;
// }
// }
// if !output_vec.2.is_empty() {
// if begun {
// output_text += " - ";
// } else {
// begun = true;
// }
// output_text += output_vec.2.join(" - ").as_str();
// }
// if !output_vec.1.is_empty() {
// if begun {
// output_text += " - ";
// } else {
// begun = true;
// }
// output_text += output_vec.1.join(" - ").as_str();
// }
// if !output_vec.0.is_empty() {
// if begun {
// output_text += " - ";
// }
// output_text += output_vec.0.join(" - ").as_str();
// }
// }
// } else {
// if write_agregation_details {
// match ag {
// Some(a) => {
// output_text += line
// .replace(
// format!("{{{}}}", a.0).as_str(),
// format!("{:.2}", a.2).as_str(),
// )
// .replace("{count}", count.to_string().as_str())
// .as_str();
// begun = true;
// }
// None => output_text += line,
// };
// }
// }
// }
//
// let mut perfdata = " |".to_string();
// match &self.leaf.data {
// Some(d) => {
// metrics.iter().for_each(|m| {
// perfdata += format!(
// " '{}'={}{};{};{};{};{}",
// m.name,
// m.value,
// d.uom,
// match m.warning {
// Some(m) => m.to_string(),
// None => "".to_string(),
// },
// match m.critical {
// Some(m) => m.to_string(),
// None => "".to_string(),
// },
// match d.min {
// Some(m) => m.to_string(),
// None => "".to_string(),
// },
// match d.max {
// Some(m) => m.to_string(),
// None => "".to_string(),
// },
// )
// .as_str();
// });
// }
// None => {
// metrics.iter().for_each(|m| {
// perfdata += format!(
// " '{}'={};{};{}",
// m.name,
// m.value,
// match m.warning {
// Some(v) => v.to_string(),
// None => "".to_string(),
// },
// match m.critical {
// Some(v) => v.to_string(),
// None => "".to_string(),
// }
// )
// .as_str();
// });
// }
// };
// output_text + &perfdata
// }
}

View File

@ -1,4 +1,4 @@
extern crate clap;
extern crate lexopt;
extern crate lalrpop_util;
extern crate log;
extern crate rasn;
@ -11,8 +11,8 @@ extern crate serde_json;
mod generic;
mod lib;
use clap::Parser;
use generic::{Command, CommandExt};
use std::ffi::{OsString, OsStr};
use lalrpop_util::lalrpop_mod;
use lib::r_snmp_get;
use serde_json::Result;
@ -20,35 +20,35 @@ use std::fs;
lalrpop_mod!(grammar);
#[derive(Parser, Debug)]
#[command(version, about)]
#[derive(Debug)]
//#[command(version, about)]
struct Cli {
/// Hostname to operate on
#[arg(long, short = 'H', default_value = "localhost")]
//#[arg(long, short = 'H', default_value = "localhost")]
hostname: String,
#[arg(long, short, default_value_t = 161)]
//#[arg(long, short, default_value_t = 161)]
port: u16,
#[arg(long, short = 'v', default_value = "2c")]
//#[arg(long, short = 'v', default_value = "2c")]
snmp_version: String,
#[arg(long, short, default_value = "public")]
//#[arg(long, short, default_value = "public")]
community: String,
#[arg(long, short)]
//#[arg(long, short)]
json_conf: String,
#[arg(long, short)]
//#[arg(long, short)]
warning_core: Option<String>,
#[arg(long, short = 'C')]
//#[arg(long, short = 'C')]
critical_core: Option<String>,
#[arg(long, short = 'a')]
//#[arg(long, short = 'a')]
warning_agregation: Option<String>,
#[arg(long, short = 'b')]
//#[arg(long, short = 'b')]
critical_agregation: Option<String>,
}
@ -67,19 +67,65 @@ fn json_to_command(file_name: &str) -> Result<Command> {
}
fn main() {
let cli = Cli::parse();
let url = format!("{}:{}", cli.hostname, cli.port);
let cmd = json_to_command(&cli.json_conf);
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_env();
let mut hostname = "localhost".to_string();
let mut port = 161;
let mut json = None;
loop {
let arg = parser.next();
match arg {
Ok(arg) => {
println!("{:?} ok", arg);
match arg {
Some(arg) => {
match arg {
Short('H') | Long("hostname") => {
hostname = parser.value().unwrap().into_string().unwrap();
},
Short('p') | Long("port") => {
port = parser.value().unwrap().parse::<u16>().unwrap();
println!("port: {}", port);
},
Short('j') | Long("json") => {
json = Some(parser.value().unwrap().into_string().unwrap());
println!("json: {:?}", json);
},
_ => {
println!("other");
}
}
},
None => {
break;
}
}
},
Err(err) => {
println!("err: {:?}", err);
std::process::exit(3);
}
}
}
let url = format!("{}:{}", hostname, port);
if json.is_none() {
println!("json is empty");
std::process::exit(3);
}
let json = json.unwrap();
let cmd = json_to_command(&json);
let cmd = cmd.unwrap();
let ext = CommandExt {
warning_core: cli.warning_core,
critical_core: cli.critical_core,
warning_agregation: cli.warning_agregation,
critical_agregation: cli.critical_agregation,
};
let result = cmd.execute(&url, &cli.snmp_version, &cli.community, &ext);
println!("{}", result.output);
std::process::exit(result.status as i32);
//let ext = CommandExt {
// warning_core: cli.warning_core,
// critical_core: cli.critical_core,
// warning_agregation: cli.warning_agregation,
// critical_agregation: cli.critical_agregation,
//};
//let result = cmd.execute(&url, &cli.snmp_version, &cli.community, &ext);
//println!("{}", result.output);
//std::process::exit(result.status as i32);
}
mod Test {