diff --git a/experimental/.gitignore b/experimental/.gitignore new file mode 100644 index 000000000..b397ff14f --- /dev/null +++ b/experimental/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/* diff --git a/experimental/Cargo.toml b/experimental/Cargo.toml new file mode 100644 index 000000000..2b0deebbe --- /dev/null +++ b/experimental/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "conn" +version = "1.1.0" + +[lib] +crate-type = ["cdylib"] + +[[bin]] +name = "generic-snmp" +path = "src/main.rs" + +[dependencies] +regex = "*" +rasn = "*" +rasn-snmp = "*" +rasn-smi = "*" +log = "*" +clap = { version = "4.5.26", features = ["derive"] } +serde = { version = "1.0.104", features = ["derive"] } +serde_json = "1.0.44" diff --git a/experimental/perl/with-ffi.pl b/experimental/perl/with-ffi.pl new file mode 100755 index 000000000..e1d5e4450 --- /dev/null +++ b/experimental/perl/with-ffi.pl @@ -0,0 +1,34 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use lib '/home/david/perl5/lib/perl5'; +use FFI::Platypus 2.00; +my $ffi = FFI::Platypus->new(api => 2, lang => => 'Rust'); + +$ffi->lib( + '../target/debug/libconn.so'); + +### Types ### +$ffi->type('object(SnmpResult)' => 'snmpresult_t'); +$ffi->type('object(SnmpVariable)' => 'snmpvariable_t'); + +### Global functions ### +$ffi->attach(snmp_get => ['string', 'string', 'string'] => 'snmpresult_t'); +$ffi->attach(snmp_walk => ['string', 'string'] => 'snmpresult_t'); + +$ffi->attach(snmpresult_variables_count => ['snmpresult_t'] => 'usize'); +$ffi->attach(snmpresult_get_variable => ['snmpresult_t', 'usize'] => 'snmpvariable_t'); +$ffi->attach( snmpresult_DESTROY => [ 'snmpresult_t' ] ); + +$ffi->attach(snmpvariable_get_name => ['snmpvariable_t'] => 'string'); +$ffi->attach(snmpvariable_get_value => ['snmpvariable_t'] => 'string'); +$ffi->attach( snmpvariable_DESTROY => [ 'snmpvariable_t' ] ); + +### Main program ### + +my $result = snmp_walk('127.0.0.1:161', '1.3.6.1.2.1.25.3.3.1.2'); +for (my $i = 0; $i < snmpresult_variables_count($result); $i++) { + my $variable = snmpresult_get_variable($result, $i); + print snmpvariable_get_name($variable) . " => " . snmpvariable_get_value($variable) . "\n"; +} diff --git a/experimental/src/generic/mod.rs b/experimental/src/generic/mod.rs new file mode 100644 index 000000000..13d70eb05 --- /dev/null +++ b/experimental/src/generic/mod.rs @@ -0,0 +1,113 @@ +extern crate serde; +extern crate serde_json; + +use serde::Deserialize; +use std::collections::{BTreeMap, HashMap}; +use lib::{r_snmp_walk, SnmpResult}; + +#[derive(Deserialize, Debug, Clone, Copy)] +enum Operation { + None, + Average, +} + +#[derive(Deserialize, Debug)] +enum QueryType { + Walk, +} + +#[derive(Deserialize, Debug)] +struct EntryOperation { + name: String, + op: Operation, +} + +#[derive(Deserialize, Debug)] +struct EntryQuery { + name: String, + oid: String, + query: QueryType, +} + +#[derive(Deserialize, Debug)] +enum Entry { + Agregation(EntryOperation), + Query(EntryQuery), +} + +#[derive(Deserialize, Debug)] +struct Leaf { + name: String, + output: String, + entries: Vec, +} + +#[derive(Deserialize, Debug)] +pub struct Command { + leaf: Leaf, +} + +pub struct CmdResult { + +} + +impl Command { + pub fn execute(&self, target: &str) -> CmdResult { + let mut agregation = ("", Operation::None); + let mut res: Option<(&str, SnmpResult)> = None; + for entry in &self.leaf.entries { + match entry { + Entry::Agregation(op) => { + println!("Agregation: {:?}", op); + agregation = (&op.name, op.op); + }, + Entry::Query(query) => { + match query.query { + QueryType::Walk => { + res = Some((&query.name, r_snmp_walk(target, &query.oid))); + } + } + } + } + } + match res { + Some(r) => { + let mut values: Vec = Vec::new(); + let mut labels: Vec = Vec::new(); + let mut idx = 0; + r.1.variables.iter().for_each(|v| { + values.push(v.value.parse().unwrap()); + let label = r.0.replace("{idx}", &idx.to_string()); + labels.push(label); + idx += 1; + }); + let count = values.len(); + let ag = match agregation.1 { + Operation::Average => { + let sum: f32 = values.iter().sum(); + Some((agregation.0, sum / values.len() as f32)) + }, + _ => None, + }; + let metrics = self.build_metrics(&labels, &values, ag); + println!("Metrics: {:?}", metrics); + }, + None => println!("No result"), + } + CmdResult {} + } + + fn build_metrics(&self, labels: &Vec, values: &Vec, ag: Option<(&str, f32)>) -> BTreeMap { + let mut metrics: BTreeMap = BTreeMap::new(); + values.iter().enumerate().for_each(|(i, v)| { + metrics.insert(labels[i].clone(), *v); + }); + match ag { + Some(a) => { + metrics.insert(a.0.to_string(), a.1); + }, + None => (), + } + metrics + } +} diff --git a/experimental/src/lib.rs b/experimental/src/lib.rs new file mode 100644 index 000000000..adfadde98 --- /dev/null +++ b/experimental/src/lib.rs @@ -0,0 +1,341 @@ +extern crate log; +extern crate rasn; +extern crate rasn_smi; +extern crate rasn_snmp; +extern crate regex; + +use log::{info, trace, warn}; +use rasn::types::ObjectIdentifier; +use rasn_snmp::v2::GetNextRequest; +use rasn_snmp::v2::GetRequest; +use rasn_snmp::v2::Pdu; +use rasn_snmp::v2::Pdus; +use rasn_snmp::v2::VarBind; +use rasn_snmp::v2::VarBindValue; +use rasn_snmp::v2c::Message; +use std::ffi::CStr; +use std::ffi::CString; +use std::net::UdpSocket; +use std::os::raw::{c_char, c_void}; + +#[derive(Debug, PartialEq)] +pub struct SnmpResult { + pub variables: Vec, +} + +type CSnmpVariable = c_void; + +#[derive(Debug, PartialEq)] +pub struct SnmpVariable { + pub name: String, + pub value: String, +} + +impl SnmpVariable { + fn new(name: String, value: String) -> SnmpVariable { + SnmpVariable { name, value } + } +} + +type CSnmpResult = c_void; + +impl SnmpResult { + fn new() -> SnmpResult { + SnmpResult { + variables: Vec::new(), + } + } + + fn add_variable(&mut self, name: String, value: String) { + self.variables.push(SnmpVariable::new(name, value)); + } + + fn concat(&mut self, other: SnmpResult) { + self.variables.extend(other.variables); + } +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn snmpresult_DESTROY(p: *mut CSnmpResult) { + unsafe { Box::from_raw(p as *mut SnmpResult) }; +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn snmpresult_get_variable(p: *mut CSnmpResult, index: usize) -> *mut CSnmpVariable { + trace!("snmpresult_get_variable {}", index); + let p = p as *mut SnmpResult; + let p = unsafe { &mut *p }; + if index >= p.variables.len() { + return Box::into_raw(Box::new(SnmpVariable::new("".to_string(), "".to_string()))) + as *mut CSnmpVariable; + } + let v = &p.variables[index]; + Box::into_raw(Box::new(SnmpVariable { + name: v.name.clone(), + value: v.value.clone(), + })) as *mut CSnmpVariable +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn snmpresult_variables_count(p: *mut CSnmpResult) -> usize { + let p = p as *mut SnmpResult; + let p = unsafe { &mut *p }; + p.variables.len() +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn snmpvariable_DESTROY(p: *mut CSnmpVariable) { + unsafe { Box::from_raw(p as *mut SnmpVariable) }; +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn snmpvariable_get_name(p: *mut CSnmpVariable) -> *mut c_char { + let p = p as *mut SnmpVariable; + let p = unsafe { &mut *p }; + let c = CString::new(p.name.clone()).unwrap(); + c.into_raw() +} + +#[allow(non_snake_case)] +#[no_mangle] +pub extern "C" fn snmpvariable_get_value(p: *mut CSnmpVariable) -> *mut c_char { + let p = p as *mut SnmpVariable; + let p = unsafe { &mut *p }; + let c = CString::new(p.value.clone()).unwrap(); + c.into_raw() +} + +#[no_mangle] +pub extern "C" fn snmp_get(target: *const c_char, oid: *const c_char) -> *mut CSnmpResult { + if target.is_null() { + return Box::into_raw(Box::new(SnmpResult::new())) as *mut CSnmpResult; + } + let target = unsafe { CStr::from_ptr(target) }; + let target = match target.to_str() { + Ok(s) => s, + Err(_) => "", + }; + + if oid.is_null() { + return Box::into_raw(Box::new(SnmpResult::new())) as *mut CSnmpResult; + } + let oid = unsafe { CStr::from_ptr(oid) }; + let oid = match oid.to_str() { + Ok(s) => s, + Err(_) => "", + }; + + Box::into_raw(Box::new(r_snmp_get(target, oid, "public"))) as *mut CSnmpResult +} + +pub fn r_snmp_get(target: &str, oid: &str, community: &str) -> SnmpResult { + let oid_tab = oid + .split('.') + .map(|x| x.parse::().unwrap()) + .collect::>(); + + let request_id = 1; + + let variable_bindings = vec![VarBind { + name: ObjectIdentifier::new_unchecked(oid_tab.into()), + value: VarBindValue::Unspecified, + }]; + + let pdu = Pdu { + request_id, + error_status: 0, + error_index: 0, + variable_bindings, + }; + + let get_request = GetRequest(pdu); + + let message: rasn_snmp::v2c::Message = Message { + version: 1.into(), + community: community.to_string().into(), + data: get_request.into(), + }; + + // Send the message through an UDP socket + let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); + socket.connect(target).expect("connect function failed"); + let encoded = rasn::der::encode(&message).unwrap(); + let res = socket.send(&encoded).unwrap(); + assert!(res == encoded.len()); + + let mut buf = [0; 1024]; + let resp = socket.recv_from(buf.as_mut_slice()).unwrap(); + + trace!("Received {} bytes", resp.0); + assert!(resp.0 > 0); + let decoded: Message = rasn::ber::decode(&buf[0..resp.0]).unwrap(); + build_response(decoded) +} + +#[no_mangle] +pub extern "C" fn snmp_walk(target: *const c_char, oid: *const c_char) -> *mut CSnmpResult { + if target.is_null() { + return Box::into_raw(Box::new(SnmpResult::new())) as *mut CSnmpResult; + } + let target = unsafe { CStr::from_ptr(target) }; + let target = match target.to_str() { + Ok(s) => s, + Err(_) => "", + }; + + if oid.is_null() { + return Box::into_raw(Box::new(SnmpResult::new())) as *mut CSnmpResult; + } + let oid = unsafe { CStr::from_ptr(oid) }; + let oid = match oid.to_str() { + Ok(s) => s, + Err(_) => "", + }; + Box::into_raw(Box::new(r_snmp_walk(target, oid))) as *mut CSnmpResult +} + +pub fn r_snmp_walk(target: &str, oid: &str) -> SnmpResult { + let community = "public"; + let oid_tab = oid + .split('.') + .map(|x| x.parse::().unwrap()) + .collect::>(); + + let mut retval = SnmpResult::new(); + let mut request_id: i32 = 1; + + let create_next_request = |id: i32, oid: &[u32]| -> Message { + let variable_bindings = vec![VarBind { + name: ObjectIdentifier::new_unchecked(oid.to_vec().into()), + value: VarBindValue::Unspecified, + }]; + + let pdu = Pdu { + request_id: id, + error_status: 0, + error_index: 0, + variable_bindings, + }; + let get_request: GetNextRequest = GetNextRequest(pdu); + + Message { + version: 1.into(), + community: community.into(), + data: get_request.into(), + } + }; + + let mut message = create_next_request(request_id, &oid_tab); + + loop { + // Send the message through an UDP socket + let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); + socket.connect(target).expect("connect function failed"); + let encoded: Vec = rasn::der::encode(&message).unwrap(); + let res: usize = socket.send(&encoded).unwrap(); + assert!(res == encoded.len()); + + let mut buf: [u8; 1024] = [0; 1024]; + let resp: (usize, std::net::SocketAddr) = socket.recv_from(buf.as_mut_slice()).unwrap(); + + trace!("Received {} bytes", resp.0); + assert!(resp.0 > 0); + let decoded: Message = rasn::ber::decode(&buf[0..resp.0]).unwrap(); + if let Pdus::Response(resp) = &decoded.data { + let resp_oid = &resp.0.variable_bindings[0].name; + let n = resp_oid.len() - 1; + if resp_oid[0..n] != oid_tab[0..n] { + break; + } + message = create_next_request(request_id, &resp_oid); + } + retval.concat(build_response(decoded)); + request_id += 1; + } + retval +} + +fn build_response(decoded: Message) -> SnmpResult { + let mut retval = SnmpResult::new(); + + if let Pdus::Response(resp) = &decoded.data { + let vars = &resp.0.variable_bindings; + for var in vars { + let name = var.name.to_string(); + match &var.value { + VarBindValue::Unspecified => { + warn!("Unspecified"); + } + VarBindValue::NoSuchObject => { + warn!("NoSuchObject"); + } + VarBindValue::NoSuchInstance => { + warn!("NoSuchInstance"); + } + VarBindValue::EndOfMibView => { + warn!("EndOfMibView"); + } + VarBindValue::Value(value) => { + warn!("Value {:?}", &value); + match value { + rasn_smi::v2::ObjectSyntax::Simple(value) => { + info!("Simple {:?}", value); + match value { + rasn_smi::v2::SimpleSyntax::Integer(value) => { + retval.add_variable(name, value.to_string()); + } + rasn_smi::v2::SimpleSyntax::String(value) => { + // We transform the value into a rust String + retval.add_variable( + name, + String::from_utf8(value.to_vec()).unwrap(), + ); + } + _ => { + retval.add_variable(name, String::from("Other")); + } + }; + } + rasn_smi::v2::ObjectSyntax::ApplicationWide(value) => { + info!("Application {:?}", value); + } + }; + } + }; + } + } + retval +} + +mod tests { + use super::*; + + #[test] + fn test_snmp_get() { + let result = r_snmp_get("127.0.0.1:161", "1.3.6.1.2.1.1.1.0", "public"); + let expected = SnmpResult { + variables: vec![SnmpVariable::new( + "1.3.6.1.2.1.1.1.0".to_string(), + "Linux CNTR-PORT-A104 6.1.0-28-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.119-1 (2024-11-22) x86_64".to_string())], + }; + assert_eq!(result, expected); + } + + #[test] + fn test_snmp_walk() { + let result = r_snmp_walk("127.0.0.1:161", "1.3.6.1.2.1.25.3.3.1.2"); + + let re = Regex::new(r"[0-9]+").unwrap(); + assert!(result.variables.len() > 0); + for v in result.variables.iter() { + let name = &v.name; + assert!(name.starts_with("1.3.6.1.2.1.25.3.3.1.2")); + assert!(re.is_match(&v.value)); + } + } +} diff --git a/experimental/src/main.rs b/experimental/src/main.rs new file mode 100644 index 000000000..9b6ae0040 --- /dev/null +++ b/experimental/src/main.rs @@ -0,0 +1,60 @@ +extern crate clap; +extern crate log; +extern crate rasn; +extern crate rasn_smi; +extern crate rasn_snmp; +extern crate regex; +extern crate serde; +extern crate serde_json; + +mod lib; +mod generic; + +use clap::Parser; +use lib::r_snmp_get; +use serde_json::Result; +use std::fs; +use generic::{Command}; + +#[derive(Parser, Debug)] +#[command(version, about)] +struct Cli { + /// Hostname to operate on + #[arg(long, short='H')] + hostname: String, + + #[arg(long, short, default_value_t = 161)] + port: u16, + + #[arg(long, short='v')] + snmp_version: String, + + #[arg(long, short)] + community: String, + + #[arg(long, short)] + json_conf: String, +} + +fn json_to_command(file_name: &str) -> Result { + + // Transform content of the file into a string + let contents = match fs::read_to_string(file_name) + { + Ok(ret) => ret, + Err(err) => panic!("Could not deserialize the file, error code: {}", err) + }; + + let module: Result = serde_json::from_str(&contents.as_str()); + module +} + +fn main() { + let cli = Cli::parse(); + let url = format!("{}:{}", cli.hostname, cli.port); +// let result = r_snmp_get(&url, "1.3.6.1.2.1.1.1.0", &cli.community); +// println!("Hello, {:?}!", &result); + let cmd = json_to_command(&cli.json_conf); + let cmd = cmd.unwrap(); + let result = cmd.execute(&url); +} diff --git a/experimental/test.json b/experimental/test.json new file mode 100644 index 000000000..1ae246d2b --- /dev/null +++ b/experimental/test.json @@ -0,0 +1,10 @@ +{ + "leaf": { + "name": "cpu", + "output": "{status}: {count} CPU(s) average usage is {total_cpu_avg} %", + "entries": [ + { "Query": { "name": "cpu_{idx}", "oid": "1.3.6.1.2.1.25.3.3.1.2", "query": "Walk" }}, + { "Agregation": { "name": "total_cpu_avg", "op": "Average"}} + ] + } +}