diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..1497656 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,45 @@ +jobs: + release: + name: Release - ${{ matrix.platform.release_for }} + strategy: + matrix: + platform: + - release_for: FreeBSD-x86_64 + os: ubuntu-20.04 + target: x86_64-unknown-freebsd + bin: precious + name: fusioninventory-agent-FreeBSD-x86_64.tar.gz + command: build + + - release_for: Windows-x86_64 + os: windows-latest + target: x86_64-pc-windows-msvc + bin: precious.exe + name: fusioninventory-agent-Windows-x86_64.zip + command: both + + - release_for: macOS-x86_64 + os: macOS-latest + target: x86_64-apple-darwin + bin: precious + name: fusioninventory-agent-Darwin-x86_64.tar.gz + command: both + + - release_for: Linux-x86_64 + os: ubuntu-20.04 + target: x86_64-unknown-linux-gnu + bin: precious + name: fusioninventory-agent-Linux-x86_64.tar.gz + command: build + + runs-on: ${{ matrix.platform.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Build binary + uses: houseabsolute/actions-rust-cross@v0 + with: + command: ${{ matrix.platform.command }} + target: ${{ matrix.platform.target }} + args: "--locked --release" + strip: true \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..83263a6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "fusioninventory-agent" +version = "3.0.0" +authors = ["David Durieux ", "FusionInventory contributors"] +description = "Agent FusionInventory for local and remote (SNMP / ESX) inventory, have also network discovery and deploy features" +edition = "2021" +homepage = "https://fusioninventory.org/" +repository = "https://github.com/fusioninventory/fusioninventory-agent-rust" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.0" +env_logger = "0.10.0" +reqwest = { version = "0.12.4", features = ["json","blocking", "rustls-tls"], default-features = false } +tokio = { version = "1.24.2", features = ["full"] } +futures = "0.3.25" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0" +nparse = "0.0.10" +sysinfo = "0.30.11" +simple-xml = "0.1.10" +chrono = "0.4.38" +iana-time-zone = "0.1.60" +clap = { version = "4.5.4", features = ["derive"] } +toml = "0.8.12" +serde_derive = "1.0.201" +lazy_static = "1.4.0" +rocket = "0.5.0" +ctrlc = "3.4.4" +single_value_channel = "1.2.2" +regex = "1.10.4" + +[target.'cfg(unix)'.dependencies] +sysctl = "0.5.5" + +[[bin]] +name = "fusioninventory-agent" diff --git a/agent.cfg b/agent.cfg new file mode 100644 index 0000000..350e59b --- /dev/null +++ b/agent.cfg @@ -0,0 +1,82 @@ +# this configuration file is in TOML format + +[general] +# set true to run in daemon mode, false to run once +daemon = false + +[network] +proxy_url = "" +proxy_user = "" +proxy_password = "" + +cert_folder = "" +cert_file = "" +ssl_check = true + +# Connections timeout, in seconds +timeout = 180 + +[webinterface] +enable_web = true +# network IPs to listen to, "0.0.0.0" to listen on all +listen_ip = [ "0.0.0.0" ] +# listen port +port = 62354 + +[logging] +# Logger backend: stderr, file or syslog (stderr) +logger = "stderr" +# loggerlevel: info, warn, debug, error +logger_level = "info" +# log file in case logger is defined as "file" +log_file = "/var/log/fusioninventory.log" +# maximum log file size, in MB +maxsize = 20 +# Syslog facility +logfacility = "LOG_USER" + +[localinventory] +enabled = true +# set the time between 2 execution of localinventory, in seconds +contact_time = 3600 +# can set many servers, can be HTTP/HTTPS links and folder (the name will be set itself). +# For example: [ "url1", "url2", "url3", "/tmp" ] +servers = ["http://127.0.0.1/backend/fusioninventory/localinventory", "/tmp"] +# you can set multiple tags for the server +tags = [] +# define the data to not inventory. +# the list is: +# * disk +# * operatingsystem +# * software +no_types = [] +# allow to scan user home directories +scan_homedirs = false +# allow to scan user profiles +scan_profiles = false + +[networkdiscovery] +enabled = false +# set the time between 2 execution of networkdiscovery, in seconds +contact_time = 604800 +# can set many servers, can be HTTP/HTTPS links and folder (the name will be set itself). +# For example: [ "url1", "url2", "url3", "/tmp/" ] +servers = [ "http://127.0.0.1/backend/fusioninventory"] + +[networkinventory] +enabled = false +# set the time between 2 execution of networkinventory, in seconds +contact_time = 7200 +# can set many servers, can be HTTP/HTTPS links and folder (the name will be set itself). +# For example: [ "url1", "url2", "url3", "/tmp/" ] +servers = [ "http://127.0.0.1/backend/fusioninventory"] + +[deploy] +enabled = false +# set the time between 2 execution of deploy, in seconds +contact_time = 1200 +# can set many servers, can be HTTP/HTTPS links and folder (the name will be set itself). +# For example: [ "url1", "url2", "url3", "/tmp/" ] +servers = [ "http://127.0.0.1/backend/fusioninventory"] +# enable p2p feature to prevent high internet / VPN bandwidth usage +p2p = true diff --git a/src/common/config.rs b/src/common/config.rs new file mode 100644 index 0000000..5ce9d38 --- /dev/null +++ b/src/common/config.rs @@ -0,0 +1,279 @@ +// file used to read the configuration file + +use serde_derive::Deserialize; +use std::fs; +use std::process::exit; +use toml; + +// Top level struct to hold the TOML data. +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Data { + #[serde(default = "g_general_default")] + pub general: General, + #[serde(default = "g_network_default")] + pub network: Network, + #[serde(default = "g_webinterface_default")] + pub webinterface: Webinterface, + #[serde(default = "g_logging_default")] + pub logging: Logging, + #[serde(default = "g_localinventory_default")] + pub localinventory: Localinventory, + #[serde(default = "g_networkdiscovery_default")] + pub networkdiscovery: Networkdiscovery, + #[serde(default = "g_networkinventory_default")] + pub networkinventory: Networkinventory, + #[serde(default = "g_deploy_default")] + pub deploy: Deploy, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct General { + pub daemon: bool, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Network { + pub proxy_url: String, + pub proxy_user: String, + pub proxy_password: String, + pub cert_folder: String, + pub cert_file: String, + #[serde(default = "ssl_check_default")] + pub ssl_check: bool, + #[serde(default = "timeout_default")] + pub timeout: u64, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Webinterface { + #[serde(default = "enable_web_default")] + pub enable_web: bool, + pub listen_ip: Vec, + #[serde(default = "port_default")] + pub port: u16, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Logging { + #[serde(default = "logger_default")] + pub logger: String, + #[serde(default = "logger_level_default")] + pub logger_level: String, + pub log_file: String, + #[serde(default = "maxsize_default")] + pub maxsize: u64, + #[serde(default = "logfacility_default")] + pub logfacility: String, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Localinventory { + #[serde(default = "localinventory_enabled_default")] + pub enabled: bool, + #[serde(default = "localinventory_contact_time_default")] + pub contact_time: u64, + pub servers: Vec, + pub tags: Vec, + pub no_types: Vec, + pub scan_homedirs: bool, + pub scan_profiles: bool, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Networkdiscovery { + pub enabled: bool, + #[serde(default = "networkdiscovery_contact_time_default")] + pub contact_time: u64, + pub servers: Vec, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Networkinventory { + pub enabled: bool, + #[serde(default = "networkinventory_contact_time_default")] + pub contact_time: u64, + pub servers: Vec, +} + +#[derive(Deserialize, Default)] +#[serde(default)] +pub struct Deploy { + pub enabled: bool, + #[serde(default = "deploy_contact_time_default")] + pub contact_time: u64, + pub servers: Vec, + #[serde(default = "p2p_default")] + pub p2p: bool, +} + +pub fn main() -> Data { + let filename = "agent.cfg"; + + // Read the contents of the config file + let contents = match fs::read_to_string(filename) { + Ok(c) => c, + Err(_) => { + // load default configuration + log::warn!("agent.cfg filename not found, load default configuration"); + return Data { + general: g_general_default(), + network: g_network_default(), + webinterface: g_webinterface_default(), + logging: g_logging_default(), + localinventory: g_localinventory_default(), + networkdiscovery: g_networkdiscovery_default(), + networkinventory: g_networkinventory_default(), + deploy: g_deploy_default(), + } + } + }; + + // load configuration file content + let data: Data = match toml::from_str(&contents) { + Ok(d) => d, + Err(e) => { + println!("Error on load configuration `{:?}`", e); + exit(1); + } + }; + return data; +} + +fn ssl_check_default() -> bool { + true +} + +fn timeout_default() -> u64 { + 180 +} + +fn enable_web_default() -> bool { + true +} + +fn port_default() -> u16 { + 62354 +} + +fn logger_default() -> String { + "stderr".to_string() +} + +fn logger_level_default() -> String { + "info".to_string() +} + +fn maxsize_default() -> u64 { + 20 +} + +fn logfacility_default() -> String { + "LOG_USER".to_string() +} + +fn localinventory_enabled_default() -> bool { + true +} + +fn localinventory_contact_time_default() -> u64 { + 3600 +} + +fn networkdiscovery_contact_time_default() -> u64 { + 604800 +} + +fn networkinventory_contact_time_default() -> u64 { + 7200 +} + +fn deploy_contact_time_default() -> u64 { + 1200 +} + +fn p2p_default() -> bool { + true +} + +fn g_general_default() -> General { + General { + daemon: false, + } +} + +fn g_network_default() -> Network { + Network { + proxy_url: "".to_string(), + proxy_user: "".to_string(), + proxy_password: "".to_string(), + cert_folder: "".to_string(), + cert_file: "".to_string(), + ssl_check: ssl_check_default(), + timeout: timeout_default(), + } +} + +fn g_webinterface_default() -> Webinterface { + Webinterface { + enable_web: enable_web_default(), + listen_ip: Vec::new(), + port: port_default(), + + } +} + +fn g_logging_default() -> Logging { + Logging { + logger: logger_default(), + logger_level: logger_level_default(), + log_file: "".to_string(), + maxsize: maxsize_default(), + logfacility: logfacility_default(), + } +} + +fn g_localinventory_default() -> Localinventory { + Localinventory { + enabled: localinventory_enabled_default(), + contact_time: localinventory_contact_time_default(), + servers: Vec::new(), + tags: Vec::new(), + no_types: Vec::new(), + scan_homedirs: false, + scan_profiles: false, + } +} + +fn g_networkdiscovery_default() -> Networkdiscovery { + Networkdiscovery { + enabled: false, + contact_time: networkdiscovery_contact_time_default(), + servers: Vec::new(), + } +} + +fn g_networkinventory_default() -> Networkinventory { + Networkinventory { + enabled: false, + contact_time: networkinventory_contact_time_default(), + servers: Vec::new(), + } +} + +fn g_deploy_default() -> Deploy { + Deploy { + enabled: false, + contact_time: deploy_contact_time_default(), + servers: Vec::new(), + p2p: p2p_default(), + } +} + diff --git a/src/common/dmidecode.rs b/src/common/dmidecode.rs new file mode 100644 index 0000000..87cf7b0 --- /dev/null +++ b/src/common/dmidecode.rs @@ -0,0 +1,36 @@ + +pub fn get_dmidecode_data(args: &[&str]) { + let dmidecode_cmd: String = get_dmidecode_program(); + let output = std::process::Command::new(dmidecode_cmd) + .args(&*args) + // .current_dir(cwd) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .output(); + match output { + Ok(output) => { + let stdout = output.stdout.to_vec(); + let stderr = output.stderr.to_vec(); + let exit_code = output.status.code(); + }, + Err(e) => { + log::error!("Failed to run command: {}", e); + let stdout: Vec = Vec::new(); + let stderr = format!("{}", e).as_bytes().to_vec(); + let exit_code = Some(2); + }, + } + + // parse now +} + +#[cfg(target_os = "windows")] +fn get_dmidecode_program() -> String { + return String::from("dmidecode.exe"); +} + +#[cfg(not(target_os = "windows"))] +fn get_dmidecode_program() -> String { + return String::from("dmidecode"); +} + diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..a93636c --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,3 @@ +pub mod dmidecode; +pub mod config; +pub mod webserver; diff --git a/src/common/webserver.rs b/src/common/webserver.rs new file mode 100644 index 0000000..a6b4276 --- /dev/null +++ b/src/common/webserver.rs @@ -0,0 +1,189 @@ +use rocket::Rocket; +use rocket::Build; +use rocket::response::{content, Redirect}; +use rocket::fs::{FileServer, relative}; +use crate::CONFIG; +use crate::CHANNEL; +use crate::{LOCALINVENTORYNEXT, NETWORKDISCOVERYNEXT, NETWORKINVENTORYNEXT, DEPLOYNEXT}; +use std::sync::{mpsc, Mutex}; +use single_value_channel::channel_starting_with; + +use chrono::prelude::DateTime; +use chrono::Local; +use std::time::{UNIX_EPOCH, Duration}; + +pub fn init_channel() -> (mpsc::SyncSender, Mutex>) { + let channel: (mpsc::SyncSender, mpsc::Receiver) = mpsc::sync_channel(0); + (channel.0, Mutex::new(channel.1)) +} + +pub fn init_channel_u64() -> (Mutex>, single_value_channel::Updater) { + let channel = channel_starting_with(0); + (Mutex::new(channel.0), channel.1) +} + +#[get("/")] +fn default() -> Redirect { + // redirect to status page + Redirect::to("/status") +} + +#[get("/status")] +fn status() -> content::RawHtml { + let mut localinventory = "disabled"; + let mut networkdiscovery = "disabled"; + let mut networkinventory = "disabled"; + let mut deploy = "disabled"; + + let receiver_nc: std::sync::MutexGuard<'_, single_value_channel::Receiver> = LOCALINVENTORYNEXT.0.lock().unwrap(); + let date_localinventory: String = get_next_date(receiver_nc, CONFIG.localinventory.enabled); + + let receiver_nc: std::sync::MutexGuard<'_, single_value_channel::Receiver> = NETWORKDISCOVERYNEXT.0.lock().unwrap(); + let date_networkdiscovery: String = get_next_date(receiver_nc, CONFIG.networkdiscovery.enabled); + + let receiver_nc: std::sync::MutexGuard<'_, single_value_channel::Receiver> = NETWORKINVENTORYNEXT.0.lock().unwrap(); + let date_networkinventory: String = get_next_date(receiver_nc, CONFIG.networkinventory.enabled); + + let receiver_nc: std::sync::MutexGuard<'_, single_value_channel::Receiver> = DEPLOYNEXT.0.lock().unwrap(); + let date_deploy: String = get_next_date(receiver_nc, CONFIG.deploy.enabled); + + if CONFIG.localinventory.enabled { + localinventory = "enabled"; + } + if CONFIG.networkdiscovery.enabled { + networkdiscovery = "enabled"; + } + if CONFIG.networkinventory.enabled { + networkinventory = "enabled"; + } + if CONFIG.deploy.enabled { + deploy = "enabled"; + } + + content::RawHtml(format!(" + + + FusionInventory: status page + + + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatuslink to force running nownext execution planned
Localinventory{localinventory}run now{date_localinventory}
Networkdiscovery{networkdiscovery}run now{date_networkdiscovery}
Networkinventory{networkinventory}run now{date_networkinventory}
Deploy{deploy}run now{date_deploy}
+ + +")) +} + +#[get("/localinventory")] +fn run_localinventory() -> content::RawHtml { + // check if not running + let receiver = LOCALINVENTORYNEXT.0.lock().unwrap(); + let next_date: String = get_next_date(receiver, CONFIG.localinventory.enabled); + set_run_page(next_date, "localinventory".to_string()) +} + +#[get("/networkdiscovery")] +fn run_networkdiscovery() -> content::RawHtml { + // check if not running + let receiver = NETWORKDISCOVERYNEXT.0.lock().unwrap(); + let next_date: String = get_next_date(receiver, CONFIG.networkdiscovery.enabled); + set_run_page(next_date, "networkdiscovery".to_string()) +} + +#[get("/networkinventory")] +fn run_networkinventory() -> content::RawHtml { + // check if not running + let receiver = NETWORKINVENTORYNEXT.0.lock().unwrap(); + let next_date: String = get_next_date(receiver, CONFIG.networkinventory.enabled); + set_run_page(next_date, "networkinventory".to_string()) +} + +#[get("/deploy")] +fn run_deploy() -> content::RawHtml { + // check if not running + let receiver = DEPLOYNEXT.0.lock().unwrap(); + let next_date: String = get_next_date(receiver, CONFIG.deploy.enabled); + set_run_page(next_date, "deploy".to_string()) +} + +#[rocket::launch] +pub fn rocket() -> Rocket { + let figment = rocket::Config::figment() + .merge(("port", CONFIG.webinterface.port)); + + rocket::custom(figment) + .mount("/", routes![status, default]) + .mount("/now", routes![run_localinventory, run_networkdiscovery, run_networkinventory, run_deploy]) + .mount("/css", FileServer::from(relative!("src/static/webserver/css"))) + .mount("/img", FileServer::from(relative!("src/static/webserver/img"))) + // create /api/status to have in json format +} + +fn get_next_date(mut receiver: std::sync::MutexGuard<'_, single_value_channel::Receiver>, enabled: bool) -> String { + let localinventory_next = receiver.latest(); + let mut timestamp_str: String = "running".to_string(); + if localinventory_next > &0 { + let d = UNIX_EPOCH + Duration::from_secs(localinventory_next.clone()); + // Create DateTime from SystemTime + let datetime = DateTime::::from(d); + // Formats the combined date and time with the specified format string. + timestamp_str = datetime.format("%Y-%m-%d %H:%M:%S").to_string(); + println!{"Date: {}",timestamp_str}; + } + if !enabled { + timestamp_str = "disabled".to_string(); + } + return timestamp_str; +} + +fn set_run_page(next_date: String, modulename: String) -> content::RawHtml { + let content: content::RawHtml = match next_date.as_str() { + "disabled" => { + content::RawHtml(format!("

Sorry, this module is disabled by configuration

")) + }, + "running" => { + content::RawHtml(format!("

{modulename} is actualy running, I do nothing

")) + }, + _ => { + let content = content::RawHtml(format!("

Run {modulename}, Baby!

")); + let val = String::from(modulename); + CHANNEL.0.send(val).unwrap(); + return content; + }, + }; + return content; +} diff --git a/src/local_inventory_sender.rs b/src/local_inventory_sender.rs new file mode 100644 index 0000000..f01021c --- /dev/null +++ b/src/local_inventory_sender.rs @@ -0,0 +1,93 @@ +use serde_json::json; +use reqwest::header::USER_AGENT; +use crate::CONFIG; + +const USER_AGENT_VALUE: &str = "FusionInventort-agent v3.0.0"; +const SERVER_URL: &str = "http://127.0.0.1/fusionsuite/backend/v1/fusioninventory/localinventory"; + +#[tokio::main] +pub async fn send_inventory() { +println!("TEST CONFIG: {:?}", CONFIG.localinventory.servers); +println!("TEST CONFIG: {:?}", CONFIG.localinventory.enabled); + + // let mut map = HashMap::new(); + // map.insert("lang", "rust"); + // map.insert("body", "json"); + let data = &json!([ + { + "type": "chassis", + "properties": [ + { + "key": "manufacturer", + "value": "Dell" + }, + { + "key": "chassis", + "value": "Dell" + }, + { + "key": "serialnumber", + "value": "XXHTY" + }, + { + "key": "model", + "value": "" + }, + { + "key": "uuid", + "value": "" + } + ], + "children": [], + "connectedto": [] + } + ]); + + let client = reqwest::Client::new(); + let res = client.post(SERVER_URL) + .header(USER_AGENT, USER_AGENT_VALUE) + .json(data) + .send() + .await + // each response is wrapped in a `Result` type + // we'll unwrap here for simplicity + .unwrap() + .text() + .await; + + println!("{:?}", res); + + // match res.status() { + // reqwest::StatusCode::OK => { + // // on success, parse our JSON to an APIResponse + // match res.json::().await { + // Ok(parsed) => println!("Success! {:?}", parsed), + // Err(_) => println!("Hm, the response didn't match the shape we expected."), + // }; + // } + // reqwest::StatusCode::UNAUTHORIZED => { + // println!("Need to grab a new token"); + // } + // other => { + // panic!("Uh oh! Something unexpected happened: {:?}", other); + // } + // }; + + + // match res { + // Ok(..) => { + // // let response_text = res.text().await?; + // info!("Processing order response"); + // // let mut futures = vec![]; + // // for result in order.result { + // // let future = task::spawn(process_order(result)); + // // futures.push(future); + // // } + + // // join_all(futures).await; + // } + // Err(e) => { + // error!("Orders API response cannot be parsed! {}", e) + // } + // }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..030c74b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,79 @@ +#[macro_use] extern crate rocket; + +use local_inventory_sender::send_inventory; +use std::env; +use clap::Parser; +use lazy_static::lazy_static; +use crate::common::config::Data; + +mod local_inventory_sender; +mod module; +mod common; +use std::sync::mpsc; +use std::sync::Mutex; + +// Manage configuration +lazy_static! { + pub static ref CONFIG: Data = common::config::main(); + static ref CHANNEL: (mpsc::SyncSender, Mutex>) = common::webserver::init_channel(); + static ref LOCALINVENTORYNEXT: (Mutex>, single_value_channel::Updater) = common::webserver::init_channel_u64(); + static ref NETWORKDISCOVERYNEXT: (Mutex>, single_value_channel::Updater) = common::webserver::init_channel_u64(); + static ref NETWORKINVENTORYNEXT: (Mutex>, single_value_channel::Updater) = common::webserver::init_channel_u64(); + static ref DEPLOYNEXT: (Mutex>, single_value_channel::Updater) = common::webserver::init_channel_u64(); +} + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + // /// Name of the person to greet + // #[arg(short, long)] + // name: String, + + // /// Number of times to greet + // #[arg(short, long, default_value_t = 1)] + // count: u8, + + /// Run in daemon mode + #[arg(short, long)] + daemon: bool, + + /// Run in debug mode + #[arg(long)] + debug: bool, + +} + +struct NextTimes { + localinventory: u64, + networkdiscovery: u64, + networkinventory: u64, + deploy: u64, +} + +fn main() { + let args = Args::parse(); + if args.debug || CONFIG.logging.logger_level == "debug" { + env::set_var("RUST_LOG", "debug"); + } else { + env::set_var("RUST_LOG", "info"); + } + env_logger::init(); + + println!("args: {:?}", args); + // return; + + // for _ in 0..args.count { + // println!("Hello {}!", args.name) + // } + + ctrlc::set_handler(move || { + println!("received Ctrl+C, exit FusionInventory-agent"); + std::process::exit(0); + }) + .expect("Error setting Ctrl-C handler"); + + // TODO load from state file + module::common::run_modules_in_thread(); + + send_inventory(); +} diff --git a/src/module/common.rs b/src/module/common.rs new file mode 100644 index 0000000..5f56962 --- /dev/null +++ b/src/module/common.rs @@ -0,0 +1,102 @@ +use std::thread; +use std::time::{SystemTime, UNIX_EPOCH, Duration}; + +use crate::module; +use crate::common; +use crate::CONFIG; +use crate::CHANNEL; +use crate::{LOCALINVENTORYNEXT, NETWORKDISCOVERYNEXT, NETWORKINVENTORYNEXT, DEPLOYNEXT}; +use std::collections::HashMap; + +pub fn run_modules_in_thread() { + let mut running_modules = HashMap::new(); + + if CONFIG.localinventory.enabled { + running_modules.insert(String::from("localinventory"), thread::spawn(|| { + log::debug!("Run inventory in thread"); + manage_module_executions("localinventory".to_string()) + })); + } + if CONFIG.networkdiscovery.enabled { + running_modules.insert(String::from("networkdiscovery"), thread::spawn(|| { + log::debug!("Run network discovery in thread"); + manage_module_executions("networkdiscovery".to_string()) + })); + } + if CONFIG.networkinventory.enabled { + running_modules.insert(String::from("networkinventory"), thread::spawn(|| { + log::debug!("Run network inventory in thread"); + manage_module_executions("networkinventory".to_string()) + })); + } + if CONFIG.deploy.enabled { + running_modules.insert(String::from("deploy"), thread::spawn(|| { + log::debug!("Run deploy in thread"); + manage_module_executions("deploy".to_string()) + })); + } + + // Start webserver + running_modules.insert(String::from("webserver"), thread::spawn(|| { + log::debug!("Run web server"); + let _ = common::webserver::main(); + })); + + loop { + let receiver = CHANNEL.1.lock().unwrap(); + let received: String = receiver.recv().unwrap(); + println!("Got: {}", received); + if running_modules.contains_key(&received) { + println!("Run module {}", received); + running_modules.get(&received).unwrap().thread().unpark(); + } else { + println!("Module {} not running", received); + } + } +} + +fn get_current_timestamp() -> u64 { + match SystemTime::now().duration_since(UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => panic!("SystemTime before UNIX EPOCH!"), + } +} + +fn manage_module_executions(module_name: String) { + let mut next_execution_time = 0; + + loop { + log::info!("loop iteration for module: {}", module_name); + + log::info!("it's time to run the module: {}", module_name); + run_module(module_name.clone()); + + if module_name == "localinventory".to_string() { + next_execution_time = get_current_timestamp() + CONFIG.localinventory.contact_time; + let _ = LOCALINVENTORYNEXT.1.update(next_execution_time); + }; + if module_name == "networkdiscovery".to_string() { + next_execution_time = get_current_timestamp() + CONFIG.networkdiscovery.contact_time; + let _ = NETWORKDISCOVERYNEXT.1.update(next_execution_time); + }; + if module_name == "networkinventory".to_string() { + next_execution_time = get_current_timestamp() + CONFIG.networkinventory.contact_time; + let _ = NETWORKINVENTORYNEXT.1.update(next_execution_time); + }; + if module_name == "deploy".to_string() { + next_execution_time = get_current_timestamp() + CONFIG.deploy.contact_time; + let _ = DEPLOYNEXT.1.update(next_execution_time); + }; + println!("currenttimestamp: {:?}", get_current_timestamp()); + println!("Next time: {:?}", next_execution_time); + // We park with timeout as next execution time planned + thread::park_timeout(Duration::from_secs(next_execution_time - get_current_timestamp())); + let _ = LOCALINVENTORYNEXT.1.update(0); + } +} + +fn run_module(module_name: String) { + if module_name == "localinventory" { + module::localinventory::run_servers::main(); + } +} diff --git a/src/module/localinventory/data/chassis/dmidecode.rs b/src/module/localinventory/data/chassis/dmidecode.rs new file mode 100644 index 0000000..17b13b0 --- /dev/null +++ b/src/module/localinventory/data/chassis/dmidecode.rs @@ -0,0 +1,75 @@ +use std::{fs::File, io::Read}; +use nparse::*; +use std::process::{Command, Stdio}; +use std::fs; + +pub fn run_inventory() -> serde_json::Value { + let path: String = std::env::temp_dir().join("dmidecode.txt").display().to_string(); + + // dmidecode -qt bios -t system > /tmp/dmidecode.txt + run_dmidecode_cmd(&path); + let mut out = String::new(); + { + let mut f = File::open(path.clone()).unwrap(); + f.read_to_string(&mut out).unwrap(); + } + let result = out.indent_to_json(); + let result = result.unwrap(); + + let _ = delete_file(&path); + return serde_json::json!([ + { + "key": "manufacturer", + "value": result["System Information"]["Manufacturer"] + }, + { + "key": "chasis", + "value": result["Chassis Information"]["Type"] + }, + { + "key": "serialnumber", + "value": result["System Information"]["Serial Number"] + }, + { + "key": "model", + "value": result["System Information"]["Product Name"] + }, + { + "key": "uuid", + "value": result["System Information"]["UUID"] + } + ]); +} + +#[cfg(target_os = "windows")] +fn run_dmidecode_cmd(path: String) -> bool { + let file = File::create(path).unwrap(); + let stdio = Stdio::from(file); + + let status = Command::new("dmidecode.exe").arg("-qt").arg("bios").arg("-t").arg("system").arg(">").arg(path).stdout(stdio).status().expect("No such file or directory"); + + if status.code() != 0 { + log::error!("dmidecode.exe command not found"); + return false; + } + return true; +} + +#[cfg(not(target_os = "windows"))] +fn run_dmidecode_cmd(path: &String) -> bool { + let file = File::create(path).unwrap(); + let stdio = Stdio::from(file); + + let status = Command::new("dmidecode").arg("-qt").arg("chassis").arg("-t").arg("system").arg(">").arg(path).stdout(stdio).status().expect("No such file or directory"); + + if status.code() != Some(0) { + log::error!("dmidecode command not found"); + return false; + } + return true; +} + +fn delete_file(path: &String) -> std::io::Result<()> { + fs::remove_file(path)?; + Ok(()) +} diff --git a/src/module/localinventory/data/chassis/mod.rs b/src/module/localinventory/data/chassis/mod.rs new file mode 100644 index 0000000..acd3293 --- /dev/null +++ b/src/module/localinventory/data/chassis/mod.rs @@ -0,0 +1 @@ +pub mod dmidecode; \ No newline at end of file diff --git a/src/module/localinventory/data/cpu/dmidecode.rs b/src/module/localinventory/data/cpu/dmidecode.rs new file mode 100644 index 0000000..06ffb5b --- /dev/null +++ b/src/module/localinventory/data/cpu/dmidecode.rs @@ -0,0 +1,139 @@ +use std::{fs::File, io::Read}; +use nparse::*; +use std::process::{Command, Stdio}; +use std::fs; + +pub fn run_inventory() -> serde_json::Value { + let path: String = std::env::temp_dir().join("dmidecode.txt").display().to_string(); + + let mut cachel1 = serde_json::json!(null); + let mut cachel2 = serde_json::json!(null); + let mut cachel3 = serde_json::json!(null); + + run_dmidecode_cmd(&path); + let mut out = String::new(); + { + let mut f = File::open(path.clone()).unwrap(); + f.read_to_string(&mut out).unwrap(); + } + + let parts = out.split("\n\n"); + for part in parts { + let result = part.to_string().indent_to_json(); + let result = result.unwrap(); + + if result["Cache Information"]["Socket Designation"] == "L1 - Cache" { + cachel1 = result["Cache Information"]["Installed Size"].clone(); + } else if result["Cache Information"]["Socket Designation"] == "L2 - Cache" { + cachel2 = result["Cache Information"]["Installed Size"].clone(); + } else if result["Cache Information"]["Socket Designation"] == "L3 - Cache" { + cachel3 = result["Cache Information"]["Installed Size"].clone(); + } + } + + // load all to have processor + let result = out.indent_to_json(); + let result = result.unwrap(); + + let _ = delete_file(&path); + return serde_json::json!([ + { + "key": "manufacturer", + "value": result["Processor Information"]["Manufacturer"] + }, + { + "key": "id", + "value": result["Processor Information"]["ID"] + }, + { + "key": "family", + "value": result["Processor Information"]["Family"] + }, + { + "key": "speed", + "type": "integer", + "unit": "Mhz", + "value": result["Processor Information"]["Max Speed"] + }, + { + "key": "serialnumber", + "value": result["Processor Information"]["Serial Number"] + }, + { + "key": "corecount", + "type": "integer", + "value": result["Processor Information"]["Core Count"] + }, + { + "key": "threadcount", + "type": "integer", + "value": result["Processor Information"]["Thread Count"] + }, + { + "key": "flags", + "type": "list", + "value": [] // TODO + }, + { + "key": "characteristics", + "type": "list", + "value": [] // TODO + }, + { + "key": "L1 cache", + "type": "integer", + "value": cachel1 + }, + { + "key": "L2 cache", + "type": "integer", + "value": cachel2 + }, + { + "key": "L2 cache", + "type": "integer", + "value": cachel3 + } + ]); +} + +#[cfg(target_os = "windows")] +fn run_dmidecode_cmd(path: String) -> bool { + let file = File::create(path).unwrap(); + let stdio = Stdio::from(file); + + let status = Command::new("dmidecode.exe").arg("-qt").arg("processor").arg(">").arg(path).stdout(stdio).status().expect("No such file or directory"); + + if status.code() != 0 { + log::error!("dmidecode.exe command not found"); + return false; + } + return true; +} + +#[cfg(not(target_os = "windows"))] +fn run_dmidecode_cmd(path: &String) -> bool { + let file = File::create(path).unwrap(); + let stdio = Stdio::from(file); + + let cmd = "dmidecode"; + let args = ["-qt", "processor", "-t", "cache"]; + let output = Command::new(cmd).args(&args).output(); + let output = output.unwrap(); + let output = String::from_utf8_lossy(&output.stdout[..]).to_string(); + + let args = ["-qt", "processor", "-t", "cache"]; + + let status = Command::new("dmidecode").args(&args).arg(">").arg(path).stdout(stdio).status().expect("No such file or directory"); + + if status.code() != Some(0) { + log::error!("dmidecode command not found"); + return false; + } + return true; +} + +fn delete_file(path: &String) -> std::io::Result<()> { + fs::remove_file(path)?; + Ok(()) +} diff --git a/src/module/localinventory/data/cpu/mod.rs b/src/module/localinventory/data/cpu/mod.rs new file mode 100644 index 0000000..acd3293 --- /dev/null +++ b/src/module/localinventory/data/cpu/mod.rs @@ -0,0 +1 @@ +pub mod dmidecode; \ No newline at end of file diff --git a/src/module/localinventory/data/filesystem/freebsd.rs b/src/module/localinventory/data/filesystem/freebsd.rs new file mode 100644 index 0000000..f527f38 --- /dev/null +++ b/src/module/localinventory/data/filesystem/freebsd.rs @@ -0,0 +1,65 @@ +#![cfg(target_os = "freebsd")] + +use std::process::{Command, Stdio}; +use regex::Regex; +use std::collections::HashMap; + +pub fn run_inventory() -> Vec { + let filesystems = get_mounted(); + fill_properties(filesystems) +} + +fn get_mounted() -> Vec> { + + let cmd = "mount"; + let args = ["-v"]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("mount -v command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let mut filesystems: Vec> = Vec::new(); + + let re: Regex = Regex::new(r"^(\/dev[\w\/]+) on ([\w\/]+) \((\w+)").unwrap(); + for line in data.lines() { + if let Some(mat) = re.captures(line) { + let mut fs_attr: HashMap = HashMap::new(); + fs_attr.insert(String::from("name"), mat[2].trim().to_string()); + fs_attr.insert(String::from("partition"), mat[1].trim().to_string()); + fs_attr.insert(String::from("type"), mat[3].trim().to_string()); + filesystems.push(fs_attr); + } + } + return filesystems; +} + +fn fill_properties(filesystems: Vec>) -> Vec { + let mut filesystems_prop = Vec::new(); + + for fs in filesystems.iter() { + filesystems_prop.push(serde_json::json!([ + { + "key": "type", + "value": fs.get("type") + }, + { + "key": "name", + "value": fs.get("name") + }, + { + "key": "partition", + "value": fs.get("partition") + } + ])); + } + + return filesystems_prop; +} diff --git a/src/module/localinventory/data/filesystem/linux.rs b/src/module/localinventory/data/filesystem/linux.rs new file mode 100644 index 0000000..39b779c --- /dev/null +++ b/src/module/localinventory/data/filesystem/linux.rs @@ -0,0 +1,63 @@ +#![cfg(target_os = "linux")] + +use std::process::{Command, Stdio}; +use regex::Regex; +use std::collections::HashMap; + +pub fn run_inventory() -> Vec { + let filesystems = get_mounted(); + fill_properties(filesystems) +} + +fn get_mounted() -> Vec> { + + let cmd = "mount"; + + let output = Command::new(cmd) + .stdout(Stdio::piped()) + .output() + .expect("mount -v command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let mut filesystems: Vec> = Vec::new(); + + let re: Regex = Regex::new(r"^(\/dev[\w\/]+) on ([\w\/]+) type (\w+)").unwrap(); + for line in data.lines() { + if let Some(mat) = re.captures(line) { + let mut fs_attr: HashMap = HashMap::new(); + fs_attr.insert(String::from("name"), mat[2].trim().to_string()); + fs_attr.insert(String::from("partition"), mat[1].trim().to_string()); + fs_attr.insert(String::from("type"), mat[3].trim().to_string()); + filesystems.push(fs_attr); + } + } + return filesystems; +} + +fn fill_properties(filesystems: Vec>) -> Vec { + let mut filesystems_prop = Vec::new(); + + for fs in filesystems.iter() { + filesystems_prop.push(serde_json::json!([ + { + "key": "type", + "value": fs.get("type") + }, + { + "key": "name", + "value": fs.get("name") + }, + { + "key": "partition", + "value": fs.get("partition") + } + ])); + } + + return filesystems_prop; +} diff --git a/src/module/localinventory/data/filesystem/mod.rs b/src/module/localinventory/data/filesystem/mod.rs new file mode 100644 index 0000000..a107213 --- /dev/null +++ b/src/module/localinventory/data/filesystem/mod.rs @@ -0,0 +1,2 @@ +pub mod freebsd; +pub mod linux; diff --git a/src/module/localinventory/data/mod.rs b/src/module/localinventory/data/mod.rs new file mode 100644 index 0000000..88fc525 --- /dev/null +++ b/src/module/localinventory/data/mod.rs @@ -0,0 +1,8 @@ +pub mod chassis; +pub mod cpu; +pub mod physicaldisk; +pub mod partition; +pub mod operatingsystem; +pub mod software; +pub mod volume; +pub mod filesystem; diff --git a/src/module/localinventory/data/operatingsystem/common.rs b/src/module/localinventory/data/operatingsystem/common.rs new file mode 100644 index 0000000..6d20563 --- /dev/null +++ b/src/module/localinventory/data/operatingsystem/common.rs @@ -0,0 +1,46 @@ +use sysinfo::System; +use chrono::Local; + +pub fn get_os_name() -> String { + match System::name() { + Some(output) => output, + _ => String::from(""), + } +} + +pub fn get_cpu_arch() -> String { + match System::cpu_arch() { + Some(output) => output, + _ => String::from(""), + } +} + +pub fn get_os_version() -> String { + match System::long_os_version() { + Some(output) => output, + _ => String::from(""), + } +} + +pub fn get_hostname() -> String { + match System::host_name() { + Some(output) => output, + _ => String::from(""), + } +} + +pub fn get_boottime() -> u64 { + System::boot_time() +} + +pub fn get_timezone_name() -> String { + match iana_time_zone::get_timezone() { + Ok(name) => name, + Err(e) => String::from(""), + } +} + +pub fn get_timezone_offset() -> i32 { + let offset_in_sec = Local::now().offset().local_minus_utc(); + return offset_in_sec; +} diff --git a/src/module/localinventory/data/operatingsystem/freebsd.rs b/src/module/localinventory/data/operatingsystem/freebsd.rs new file mode 100644 index 0000000..722c6de --- /dev/null +++ b/src/module/localinventory/data/operatingsystem/freebsd.rs @@ -0,0 +1,83 @@ +#![cfg(target_os = "freebsd")] +use std::fs; +use chrono::prelude::{DateTime, Utc}; +// use std::process::Command; +use crate::module::localinventory::data::operatingsystem::common::*; + +pub fn run_inventory() -> serde_json::Value { + let osname: String = get_os_name(); + let osversion: String = get_os_version(); + // split os_version in OS version + servicepack/patch level + + let cpuarch: String = get_cpu_arch(); + let hostname: String = get_hostname(); + let lastboottime: u64 = get_boottime(); + let timezone: String = get_timezone_name(); + let timezoneoffset: i32 = get_timezone_offset(); + let installdate: String = get_installation_date(); + + // perhaps get the KELNEL ident: uname -i + // kernel version: uname -k + // + return fill_properties(osname, osversion, cpuarch, hostname, lastboottime, timezone, timezoneoffset, installdate) +} + +fn get_installation_date() -> String { + match fs::metadata("/var/log/bsdinstall_log") { + Ok(metadata) => { + let dt: DateTime = metadata.modified().unwrap().clone().into(); + dt.format("%Y-%m-%d").to_string() + }, + Err(e) => { + String::from("") + } + } +} + +fn fill_properties(osname: String, osversion: String, cpuarch: String, hostname: String, + lastboottime: u64, timezone: String, timezoneoffset: i32, installdate: String) -> serde_json::Value { + serde_json::json!([ + { + "key": "completename", + "value": osname + }, + { + "key": "version", + "value": osversion + }, + { + "key": "architecture", + "value": cpuarch + }, + { + "key": "servicepack", + "value": "" + }, + { + "key": "installationdate", + "value": installdate + }, + { + "key": "hostname", + "value": hostname + }, + { + "key": "domain", + "value": "" + }, + { + "key": "lastboottime", + "type": "integer", + "unit": "s", + "value": lastboottime + }, + { + "key": "timezonename", + "value": timezone + }, + { + "key": "timezoneutcoffset", + "value": timezoneoffset + } + ]) +} diff --git a/src/module/localinventory/data/operatingsystem/linux.rs b/src/module/localinventory/data/operatingsystem/linux.rs new file mode 100644 index 0000000..894363b --- /dev/null +++ b/src/module/localinventory/data/operatingsystem/linux.rs @@ -0,0 +1,68 @@ +#![cfg(target_os = "linux")] +use std::fs; +use chrono::prelude::{DateTime, Utc}; +// use std::process::Command; +use crate::module::localinventory::data::operatingsystem::common::*; + +pub fn run_inventory() -> serde_json::Value { + let osname: String = get_os_name(); + let osversion: String = get_os_version(); + // split os_version in OS version + servicepack/patch level + + let cpuarch: String = get_cpu_arch(); + let hostname: String = get_hostname(); + let lastboottime: u64 = get_boottime(); + let timezone: String = get_timezone_name(); + let timezoneoffset: i32 = get_timezone_offset(); + let installdate: String = "".to_string(); + + return fill_properties(osname, osversion, cpuarch, hostname, lastboottime, timezone, timezoneoffset, installdate) +} + +fn fill_properties(osname: String, osversion: String, cpuarch: String, hostname: String, + lastboottime: u64, timezone: String, timezoneoffset: i32, installdate: String) -> serde_json::Value { + serde_json::json!([ + { + "key": "completename", + "value": osname + }, + { + "key": "version", + "value": osversion + }, + { + "key": "architecture", + "value": cpuarch + }, + { + "key": "servicepack", + "value": "" + }, + { + "key": "installationdate", + "value": installdate + }, + { + "key": "hostname", + "value": hostname + }, + { + "key": "domain", + "value": "" + }, + { + "key": "lastboottime", + "type": "integer", + "unit": "s", + "value": lastboottime + }, + { + "key": "timezonename", + "value": timezone + }, + { + "key": "timezoneutcoffset", + "value": timezoneoffset + } + ]) +} diff --git a/src/module/localinventory/data/operatingsystem/mod.rs b/src/module/localinventory/data/operatingsystem/mod.rs new file mode 100644 index 0000000..9812a9b --- /dev/null +++ b/src/module/localinventory/data/operatingsystem/mod.rs @@ -0,0 +1,3 @@ +pub mod common; +pub mod freebsd; +pub mod linux; diff --git a/src/module/localinventory/data/partition/freebsd.rs b/src/module/localinventory/data/partition/freebsd.rs new file mode 100644 index 0000000..2a219dc --- /dev/null +++ b/src/module/localinventory/data/partition/freebsd.rs @@ -0,0 +1,134 @@ +#![cfg(target_os = "freebsd")] +use sysctl::Sysctl; + +pub fn run_inventory(disk: serde_json::Value) -> Vec { + let xml = load_geom_data_xml(); + match xml { + Some(x) => { + return fill_properties(disk, x); + } + None => { + println!("No data"); + return Vec::::new(); + } + } +} + +fn load_geom_data_xml() -> Option { + + let ctl = match sysctl::Ctl::new("kern.geom.confxml") { + Ok(f) => f, + Err(e) => { + log::error!("Read sysctl kern.geom.confxml have error: {:?}", e); + return None; + } + }; + + let val = ctl.value_string().unwrap(); + match simple_xml::from_string(val.as_str()) { + Ok(f) => Some(f), + Err(e) => { + log::error!("Unable to read XML string of kern.geom.confxml: {:?}", e); + None + } + } +} + +fn fill_properties(disk: serde_json::Value, xml: simple_xml::Node) -> Vec { + + let mut parts = Vec::new(); + let mut disk_id = String::from("test"); + let disks = disk.as_array(); + for disk in disks { + for property in disk { + if property["key"] == "id" { + disk_id = property["value"].to_string(); + break; + } + } + } + log::debug!("THE ID {}", disk_id); + + for i in &xml["class"] { + if i["name"][0].content == "PART" { + for j in &i["geom"] { + let provider = &j["consumer"][0]["provider"][0]; + if disk_id.trim_matches('"') == provider.attributes["ref"].trim_end_matches('"') { + for p in &j["provider"] { + let data = serde_json::json!([ + { + "key": "id", + "value": p["config"][0]["rawuuid"][0].content, + }, + { + "key": "creationdate", + "value": "", + }, + { + "key": "description", + "value": "", + }, + { + "key": "size", + "type": "integer", + "unit": "B", + "value": p["config"][0]["length"][0].content, + }, + { + "key": "usedsize", + "value": "", + }, + { + "key": "freesize", + "value": "", + }, + { + "key": "filesystem", + "value": "", + }, + { + "key": "label", + "value": p["config"][0]["label"][0].content, + }, + { + "key": "mountpoint", + "value": "", + }, + { + "key": "serial", + "value": "", + }, + { + "key": "system", + "value": "", + }, + { + "key": "ostype", + "value": p["config"][0]["type"][0].content, + }, + // GET INFO in ELI class + { + "key": "encryption", + "value": "", + }, + { + "key": "algorithm", + "value": "", + }, + { + "key": "encryptedstatus", + "value": "", + }, + { + "key": "encryptedtype", + "value": "", + } + ]); + parts.push(data); + } + } + } + } + } + return parts; +} diff --git a/src/module/localinventory/data/partition/linux.rs b/src/module/localinventory/data/partition/linux.rs new file mode 100644 index 0000000..b4933b0 --- /dev/null +++ b/src/module/localinventory/data/partition/linux.rs @@ -0,0 +1,110 @@ +#![cfg(target_os = "linux")] +use std::process::{Command, Stdio}; + +// TODO +// get partitions +// get LVM data, with layers +// on last layer, run operatingsystem + +pub fn run_inventory(disk: serde_json::Value) -> Vec { + log::debug!("[partition] run linux"); + let data = load_lshw_data_json(); + fill_properties(disk, data) +} + +fn load_lshw_data_json() -> Vec { + let cmd = "lshw"; + let args = ["-json", "-class", "volume"]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("lshw command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let parts: Vec = serde_json::from_str(&data).expect("JSON was not well-formatted in lshw output command"); + return parts; +} + +fn fill_properties(disk: serde_json::Value, data: Vec) -> Vec { + + let mut parts = Vec::new(); + for datapart in data { + let part = serde_json::json!([ + { + "key": "id", + "value": datapart["id"], + }, + { + "key": "creationdate", + "value": "", + }, + { + "key": "description", + "value": datapart["description"], + }, + { + "key": "size", + "type": "integer", + "unit": "B", + "value": datapart["size"], + }, + { + "key": "usedsize", + "value": "", + }, + { + "key": "freesize", + "value": "", + }, + { + "key": "filesystem", + "value": "", + }, + { + "key": "label", + "value": datapart["logicalname"], + }, + { + "key": "mountpoint", + "value": "", + }, + { + "key": "serial", + "value": datapart["serial"], + }, + { + "key": "system", + "value": "", + }, + { + "key": "ostype", + "value": "", + }, + { + "key": "encryption", + "value": "", + }, + { + "key": "algorithm", + "value": "", + }, + { + "key": "encryptedstatus", + "value": "", + }, + { + "key": "encryptedtype", + "value": "", + } + ]); + parts.push(part); + } + return parts; +} diff --git a/src/module/localinventory/data/partition/mod.rs b/src/module/localinventory/data/partition/mod.rs new file mode 100644 index 0000000..e85d159 --- /dev/null +++ b/src/module/localinventory/data/partition/mod.rs @@ -0,0 +1,3 @@ +pub mod freebsd; +pub mod linux; +// pub mod windows; diff --git a/src/module/localinventory/data/physicaldisk/freebsd.rs b/src/module/localinventory/data/physicaldisk/freebsd.rs new file mode 100644 index 0000000..222aa50 --- /dev/null +++ b/src/module/localinventory/data/physicaldisk/freebsd.rs @@ -0,0 +1,107 @@ +#![cfg(target_os = "freebsd")] +use sysctl::Sysctl; + +pub fn run_inventory() -> Vec { + let xml = load_geom_data_xml(); + match xml { + Some(x) => { + return fill_properties(x); + }, + None => { + println!("No data"); + return Vec::::new(); + } + } +} + +fn load_geom_data_xml() -> Option { + + let ctl = match sysctl::Ctl::new("kern.geom.confxml") { + Ok(f) => f, + Err(e) => { + log::error!("Read sysctl kern.geom.confxml have error: {:?}", e); + return None; + } + }; + + let val = ctl.value_string().unwrap(); + match simple_xml::from_string(val.as_str()) { + Ok(f) => Some(f), + Err(e) => { + log::error!("Unable to read XML string of kern.geom.confxml: {:?}", e); + None + } + } +} + +fn fill_properties(xml: simple_xml::Node) -> Vec { + + // Since there can multiple nodes/tags with the same name, we need to index twice + // let heading = &xml["heading"][0]; + // println!("XML: {:?}", &xml["class"]); + + let mut disks = Vec::new(); + + for i in &xml["class"] { + if i["name"][0].content == "DISK" { + for j in &i["geom"] { + // println!("XML: {:?}", j); + + // + // nda0 + // 1 + // + // + // + // r3w3e7 + // nda0 + // nvd0 + // 512110190592 + // 512 + // 0 + // 0 + // + // 0 + // 0 + // 0 + // 4YC4N027910704S4X + // 0000000000000000ace42e003a53a7f5 + // SKHynix_HFS512GEJ4X164N + // + // + let provider = &j["provider"][0]; + let config = &provider["config"][0]; + + let data = serde_json::json!([ + { + "key": "name", + "value": j["name"][0].content, + }, + { + "key": "description", + "value": config["descr"][0].content, + }, + { + "key": "serialnumber", + "value": config["ident"][0].content, + }, + { + "key": "size", + "type": "integer", + "unit": "B", + "value": provider["mediasize"][0].content, + }, + { + "key": "id", + "value": provider.attributes["id"], + } + ]); + disks.push(data); + } + } + } + return disks; +} + +// camcontrol identify nda0 +// pattern => qr/firmware revision[ ]+(\w+)/ diff --git a/src/module/localinventory/data/physicaldisk/linux.rs b/src/module/localinventory/data/physicaldisk/linux.rs new file mode 100644 index 0000000..83fd013 --- /dev/null +++ b/src/module/localinventory/data/physicaldisk/linux.rs @@ -0,0 +1,60 @@ +#![cfg(target_os = "linux")] +use std::process::{Command, Stdio}; + +pub fn run_inventory() -> Vec { + let data = load_lshw_data_json(); + fill_properties(data) +} + +fn load_lshw_data_json() -> Vec { + let cmd = "lshw"; + let args = ["-json", "-class", "disk"]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("lshw command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let disks: Vec = serde_json::from_str(&data).expect("JSON was not well-formatted in lshw output command"); + return disks; +} + +fn fill_properties(data: Vec) -> Vec { + + let mut disks = Vec::new(); + for datadisk in data { + let disk = serde_json::json!([ + { + "key": "name", + "value": datadisk["product"], + }, + { + "key": "description", + "value": datadisk["description"], + }, + { + "key": "serialnumber", + "value": datadisk["serial"], + }, + { + "key": "size", + "type": "integer", + "unit": "B", + "value": 0, + }, + { + "key": "id", + "value": datadisk["id"], + } + ]); + disks.push(disk); + } + return disks; +} diff --git a/src/module/localinventory/data/physicaldisk/mod.rs b/src/module/localinventory/data/physicaldisk/mod.rs new file mode 100644 index 0000000..43c10be --- /dev/null +++ b/src/module/localinventory/data/physicaldisk/mod.rs @@ -0,0 +1,3 @@ +pub mod freebsd; +pub mod linux; +pub mod windows; diff --git a/src/module/localinventory/data/physicaldisk/windows.rs b/src/module/localinventory/data/physicaldisk/windows.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/module/localinventory/data/software/freebsd.rs b/src/module/localinventory/data/software/freebsd.rs new file mode 100644 index 0000000..dcbabff --- /dev/null +++ b/src/module/localinventory/data/software/freebsd.rs @@ -0,0 +1,111 @@ +#![cfg(target_os = "freebsd")] +use std::process::{Command, Stdio}; +// use chrono::prelude::{DateTime, Utc}; + +pub fn run_inventory() -> Vec { + + let packages = load_from_pkg(); + fill_properties(packages) +} + +fn load_from_pkg() -> Vec { + + let mut packages = Vec::new(); + let cmd = "pkg"; + let args = ["info", "--raw", "--raw-format", "json-compact", "--all"]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("pkg command error"); + + // split + let empty = String::from(""); + let linesstr = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let lines = linesstr.lines(); + for line in lines { + if line != "" { + let json_test: serde_json::Value = serde_json::from_str(line).expect("JSON was not well-formatted line by line"); + packages.push(json_test); + } + } + + // TODO split each line and read json + // serde_json::from_str(&String::from_utf8_lossy(&output.stdout)).expect("JSON was not well-formatted") + // serde_json::from_slice(output.stdout.as_slice()).expect("JSON was not well-formatted") + + return packages; +} + +fn fill_properties(packages: Vec) -> Vec { + + let mut softwares = Vec::new(); + for package in packages { + // let installationdate: DateTime = package["timestamp"]; + let data = serde_json::json!([ + { + "key": "cleanedname", + "value": package["name"], + }, + { + "key": "version", + "value": package["version"], + }, + { + "key": "publisher", + "value": "", + }, + { + "key": "maintainer", + "value": package["maintainer"], + }, + { + "key": "source", + "value": package["annotations"]["repository"], + }, + { + "key": "type", + "value": "", + }, + { + "key": "architecture", + "value": package["arch"], + }, + { + "key": "category", + "value": package["categories"][0], + }, + { + "key": "installationdate", + "value": "", // installationdate.format("%Y-%m-%d").to_string(), + }, + { + "key": "uninstallcommand", + "value": String::from("pkg delete -f").push_str(&package["name"].to_string()), + }, + { + "key": "guid", + "value": "", + }, + { + "key": "comment", + "value": package["comment"], + }, + { + "key": "mainurl", + "value": package["www"], + }, + { + "key": "helpurl", + "value": "", + } + ]); + softwares.push(data); + } + return softwares; +} diff --git a/src/module/localinventory/data/software/linux.rs b/src/module/localinventory/data/software/linux.rs new file mode 100644 index 0000000..1e6d5fa --- /dev/null +++ b/src/module/localinventory/data/software/linux.rs @@ -0,0 +1,186 @@ +#![cfg(target_os = "linux")] + +use std::process::{Command, Stdio}; +use std::collections::HashMap; + +pub fn run_inventory() -> Vec { + // https://github.com/sigoden/upt + + load_from_package_manager() +} + +fn load_from_package_manager() -> Vec { + // let data = get_dpkg(); + // println!("dpkg return: {:?}", data); + let output = get_rpm(); + // println!("rpm return: {:?}", output); + let data = parse_rpm_output(output); + + fill_properties(data) +} + +fn get_dpkg() -> Option { + // https://crates.io/crates/dpkg-query-json + let args = [ + "--show", + "--showformat='${Package}\t\t${Architecture}\t\t${Version}\t\t${Installed-Size}\t\t${Section}\t\t${Status}\t\t${Homepage}\t\t${Maintainer}\t\t${binary:Summary}\n" + ]; + let output = match Command::new("dpkg-query") + .args(args) + .stdout(Stdio::piped()) + .output() + { + Ok(output) => { + if output.status.success() { + match String::from_utf8(output.stdout) { + Ok(x) => { + return Some(x); + }, + Err(_) => "".to_string(), + } + } else { + return Some("".to_string()); + } + }, + _ => { + log::debug!("apt not found, next package manager"); + return Some("".to_string()); + }, + }; + None +} + +fn get_rpm() -> String { + let args = [ + "-qa", + "--queryformat", + "%{NAME}\t\t%{ARCH}\t\t%{VERSION}-%{RELEASE}\t\t%{INSTALLTIME}\t\t%{VENDOR}\t\t%{SUMMARY}\t\t%{GROUP}\t\t%{PACKAGER}\t\t%{URL}\t\t%{BUGURL}\t\t%{PKGID}\n" + ]; + let output = Command::new("rpm") + .args(args) + .stdout(Stdio::piped()) + .output() + .expect("rpm command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + return data; +} + +fn parse_rpm_output(output: String) -> Vec> { + let mut softwares = Vec::new(); + // split each line (each line is a software) + let parts = output.split("\n"); + for part in parts { + // split + let pp: String = part.to_string(); + // let mut software_info = pp.split("\t\t"); + let software_info: Vec<&str> = pp.split("\t\t").collect(); + let mut soft = HashMap::new(); + if software_info.iter().count() < 11 { + continue; + } + + soft.insert(String::from("name"), software_info[0].to_string().clone()); + soft.insert(String::from("arch"), software_info[1].to_string().clone()); + soft.insert(String::from("version"), software_info[2].to_string().clone()); + soft.insert(String::from("installationdate"), software_info[3].to_string().clone()); + soft.insert(String::from("publisher"), software_info[4].to_string().clone()); + soft.insert(String::from("comment"), software_info[5].to_string().clone()); + soft.insert(String::from("maintainer"), software_info[7].to_string().clone()); + soft.insert(String::from("mainurl"), software_info[8].to_string().clone()); + soft.insert(String::from("helpurl"), software_info[9].to_string().clone()); + // it''s more id than uuid + soft.insert(String::from("guid"), software_info[10].to_string().clone()); + softwares.push(soft); + } + return softwares; +} + +fn fill_properties(packages: Vec>) -> Vec { + + let mut softwares = Vec::new(); + for package in packages { + // let installationdate: DateTime = package["timestamp"]; + let data = serde_json::json!([ + { + "key": "cleanedname", + "value": package.get("name"), + }, + { + "key": "version", + "value": package.get("version"), + }, + { + "key": "publisher", + "value": package.get("publisher"), + }, + { + "key": "maintainer", + "value": package.get("maintainer"), + }, + { + "key": "source", + "value": "", + }, + { + "key": "type", + "value": "", + }, + { + "key": "architecture", + "value": package["arch"], + }, + { + "key": "category", + "value": "", + }, + { + "key": "installationdate", + "value": package.get("installationdate"), // .format("%Y-%m-%d").to_string(), // installationdate.format("%Y-%m-%d").to_string(), + }, + { + "key": "uninstallcommand", + "value": "", + }, + { + "key": "guid", + "value": package.get("guid"), + }, + { + "key": "comment", + "value": package["comment"], + }, + { + "key": "mainurl", + "value": package["mainurl"], + }, + { + "key": "helpurl", + "value": package["helpurl"], + } + ]); + softwares.push(data); + } + return softwares; +} + +#[cfg(test)] +mod tests { + + #[test] + fn parse_rpm_output() { + // load data from file tests/software_rpm.data + + // send it in parser function + + + // verify content converted + + + } +} \ No newline at end of file diff --git a/src/module/localinventory/data/software/mod.rs b/src/module/localinventory/data/software/mod.rs new file mode 100644 index 0000000..a107213 --- /dev/null +++ b/src/module/localinventory/data/software/mod.rs @@ -0,0 +1,2 @@ +pub mod freebsd; +pub mod linux; diff --git a/src/module/localinventory/data/volume/freebsd.rs b/src/module/localinventory/data/volume/freebsd.rs new file mode 100644 index 0000000..1883bf3 --- /dev/null +++ b/src/module/localinventory/data/volume/freebsd.rs @@ -0,0 +1,270 @@ +#![cfg(target_os = "freebsd")] + +use std::process::{Command, Stdio}; +use regex::Regex; +use std::collections::HashMap; + +pub fn run_inventory() -> Vec { + + // GET ZFS +// zpool status -P +// pool: zroot +// state: ONLINE +// config: + +// NAME STATE READ WRITE CKSUM +// zroot ONLINE 0 0 0 +// /dev/nda0p4.eli ONLINE 0 0 0 + +// # zpool list -L -P -v +// NAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOT +// zroot 468G 110G 358G - - 9% 23% 1.00x ONLINE - +// /dev/nda0p4.eli 469G 110G 358G - - 9% 23.5% - ONLINE + + + +// zfs list +// NAME USED AVAIL REFER MOUNTPOINT +// zroot 110G 344G 96K /zroot +// zroot/ROOT 19.6G 344G 96K none +// zroot/ROOT/14.0-RELEASE_2024-05-06_182912 8K 344G 18.8G / +// zroot/ROOT/default 19.6G 344G 19.1G / +// zroot/bastille 7.52G 344G 128K /usr/local/bastille +// zroot/bastille/backups 2.74G 344G 2.74G /usr/local/bastille/backups +// zroot/bastille/cache 96K 344G 96K /usr/local/bastille/cache +// zroot/bastille/jails 4.64G 344G 104K /usr/local/bastille/jails +// zroot/bastille/jails/photoprism2 3.11G 344G 120K /usr/local/bastille/jails/photoprism2 +// zroot/bastille/jails/photoprism2/root 3.11G 344G 3.11G /usr/local/bastille/jails/photoprism2/root +// zroot/bastille/jails/rust 1.53G 344G 120K /usr/local/bastille/jails/rust +// zroot/bastille/jails/rust/root 1.53G 344G 1.53G /usr/local/bastille/jails/rust/root +// zroot/bastille/logs 96K 344G 96K /var/log/bastille +// zroot/bastille/releases 148M 344G 96K /usr/local/bastille/releases +// zroot/bastille/releases/Debian12 148M 344G 148M /usr/local/bastille/releases/Debian12 +// zroot/bastille/templates 96K 344G 96K /usr/local/bastille/templates +// zroot/datas 63.8G 344G 63.8G /datas +// zroot/home 14.0G 344G 14.0G /home +// zroot/tmp 14.9M 344G 14.9M /tmp +// zroot/usr 4.73G 344G 96K /usr +// zroot/usr/ports 3.93G 344G 3.93G /usr/ports +// zroot/usr/src 816M 344G 816M /usr/src +// zroot/var 10.3M 344G 96K /var +// zroot/var/audit 96K 344G 96K /var/audit +// zroot/var/crash 96K 344G 96K /var/crash +// zroot/var/log 5.57M 344G 5.57M /var/log +// zroot/var/mail 3.77M 344G 3.77M /var/mail +// zroot/var/tmp 648K 344G 648K /var/tmp + + + let pools = zfs_pools(); + fill_properties(pools) + +} + +fn fill_properties(pools: Vec>) -> Vec { + + let mut pools_prop = Vec::new(); + + for pool in pools.iter() { + // TODO get datasets + let datasets = zfs_datasets(pool.get("name").unwrap().clone()); + let datasets_pros = fill_properties_dataset(datasets); + pools_prop.push(serde_json::json!([ + { + "key": "type", + "value": "zpool" + }, + { + "key": "name", + "value": pool.get("name") + }, + { + "key": "partitions", + "type": "list", + "value": ["part_id1","part_id2"] + }, + { + "key": "totalsize", + "type": "integer", + "unit": pool.get("totalsize_unit"), + "value": pool.get("totalsize") + }, + { + "key": "allocatedsize", + "type": "integer", + "unit": pool.get("allocatedsize_unit"), + "value": pool.get("allocatedsize") + }, + { + "key": "freesize", + "type": "integer", + "unit": pool.get("freesize_unit"), + "value": pool.get("freesize") + }, + { + "key": "allocatedpercentage", + "type": "integer", + "unit": "%", + "value": pool.get("allocatedpercentage") + }, + { + "key": "health", + "value": pool.get("health") // from list: ONLINE, DEGRADED, FAULTED, OFFLINE, REMOVED, UNAVAIL + }, + { + "key": "_children", + "value": datasets_pros + } + ])); + } + + // TODO children, get dataset + + return pools_prop; +} + +fn zfs_pools() -> Vec> { + let cmd = "zpool"; + let args = ["list", "-L", "-P", "-v"]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("zpool list -L -P -v command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + // parsing the command output to extract information needed + let mut pools: Vec> = Vec::new(); + let mut pool_attr: HashMap = HashMap::new(); + + // let mut incr: i32 = 0; + // let mut start_config: bool = false; + let partitions: Vec = Vec::new(); + let re: Regex = Regex::new(r"^(\s*)(\S+)(?:\s+)(\d+)(\w){1}(?:\s+)(\d+)(\w){1}(?:\s+)(\d+)(\w){1}(?:\s+)(\S+)(?:\s+)(\S+)(?:\s+)(\S+)(?:\s+)([\d\.]+)%(?:\s+)(\S+)(?:\s+)(\S+)").unwrap(); + for line in data.lines() { + if let Some(mat) = re.captures(line) { + if pools.iter().count() > 0 { + pools.push(pool_attr.clone()); + + let pool_attr: HashMap = HashMap::new(); + } + if &mat[1] == "" { + // TODO get data of pool + pool_attr.insert(String::from("name"), mat[2].trim().to_string()); + pool_attr.insert(String::from("totalsize"), mat[3].to_string()); + pool_attr.insert(String::from("totalsize_unit"), mat[4].to_string()); + pool_attr.insert(String::from("allocatedsize"), mat[5].to_string()); + pool_attr.insert(String::from("allocatedsize_unit"), mat[6].to_string()); + pool_attr.insert(String::from("freesize"), mat[7].to_string()); + pool_attr.insert(String::from("freesize_unit"), mat[8].to_string()); + pool_attr.insert(String::from("allocatedpercentage"), mat[12].to_string()); + pool_attr.insert(String::from("health"), mat[14].to_string()); + } else { + // TODO now manage partitions + + } + } + } + pools.push(pool_attr); + + return pools; +} + +fn zfs_datasets(volume: String) -> Vec> { + // zfs list -r zroot + + let cmd = "zfs"; + let args = ["list", "-r", volume.as_str()]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("zfs list -r command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let mut datasets: Vec> = Vec::new(); + let mut dataset_attr: HashMap = HashMap::new(); + let re: Regex = Regex::new(r"^(\S+)(?:\s+)([\d\.]+)(\w){1}(?:\s+)([\d\.]+)(\w){1}(?:\s+)([\d\.]+)(\w){1}(?:\s+)").unwrap(); + + for line in data.lines() { + if let Some(mat) = re.captures(line) { + let mut dataset_attr: HashMap = HashMap::new(); + dataset_attr.insert(String::from("name"), mat[1].trim().to_string()); + dataset_attr.insert(String::from("type"), "dataset".to_string()); + dataset_attr.insert(String::from("allocatedsize"), mat[2].to_string()); + dataset_attr.insert(String::from("allocatedsize_unit"), mat[3].to_string()); + dataset_attr.insert(String::from("freesize"), mat[4].to_string()); + dataset_attr.insert(String::from("freesize_unit"), mat[5].to_string()); + datasets.push(dataset_attr); + } + } + // TODO fill_properties_dataset + return datasets; +} + +fn fill_properties_dataset(datasets: Vec>) -> Vec { + let mut datasets_prop = Vec::new(); + + for dataset in datasets.iter() { + datasets_prop.push(serde_json::json!([ + { + "key": "type", + "value": "zdataset" + }, + { + "key": "name", + "value": dataset.get("name") + }, + { + "key": "partitions", + "type": "list", + "value": [] + }, + { + "key": "totalsize", + "type": "integer", + "unit": "", + "value": "" + }, + { + "key": "allocatedsize", + "type": "integer", + "unit": dataset.get("allocatedsize_unit"), + "value": dataset.get("allocatedsize") + }, + { + "key": "freesize", + "type": "integer", + "unit": dataset.get("freesize_unit"), + "value": dataset.get("freesize") + }, + { + "key": "allocatedpercentage", + "type": "integer", + "unit": "%", + "value": "" + }, + { + "key": "health", + "value": "" + }, + + ])); + } + return datasets_prop; +} + +// volume: zpool +// volume: dataset +// filesystem diff --git a/src/module/localinventory/data/volume/linux.rs b/src/module/localinventory/data/volume/linux.rs new file mode 100644 index 0000000..bd5afb7 --- /dev/null +++ b/src/module/localinventory/data/volume/linux.rs @@ -0,0 +1,197 @@ +#![cfg(target_os = "linux")] +use std::process::{Command, Stdio}; +use regex::Regex; +use std::collections::HashMap; + +pub fn run_inventory() -> Vec { + + let vgs = get_vgs(); + fill_properties(vgs) +} + +fn get_vgs() -> Vec> { + let cmd = "vgs"; + let args = ["--noheading", "--nosuffix", "--units", "M", "-o", "vg_name,pv_count,lv_count,vg_attr,vg_size,vg_free,vg_uuid,vg_extent_size"]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("vgs command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + // parsing the command output to extract information needed + let mut volume_groups: Vec> = Vec::new(); + + let re: Regex = Regex::new(r"^(?:\s+)(\w*)(?:\s+)(\d+)(?:\s+)(\d+)(?:\s+)([\w-]+)(?:\s+)([\d\.]+)(?:\s+)([\d\.]+)(?:\s+)([\w-]+)(?:\s+)([\d\.]+)").unwrap(); + for line in data.lines() { + if let Some(mat) = re.captures(line) { + println!("VGS: {:?}", mat); + let mut volume_groups_attr: HashMap = HashMap::new(); + + volume_groups_attr.insert(String::from("name"), mat[1].trim().to_string()); + volume_groups_attr.insert(String::from("uuid"), mat[7].trim().to_string()); + volume_groups_attr.insert(String::from("totalsize"), mat[5].to_string()); + volume_groups_attr.insert(String::from("allocatedsize"), "".to_string()); + volume_groups_attr.insert(String::from("freesize"), mat[6].to_string()); + volume_groups_attr.insert(String::from("allocatedpercentage"), "".to_string()); + volume_groups_attr.insert(String::from("health"), "ONLINE".to_string()); + volume_groups.push(volume_groups_attr); + } + } + return volume_groups; +} + +fn get_lvs(volume_group: String) -> Vec> { + + let cmd = "lvs"; + let args = ["--noheading", "--nosuffix", "--units", "M", "-o", "lv_name,vg_uuid,lv_attr,lv_size,lv_uuid,seg_count", volume_group.as_str()]; + + let output = Command::new(cmd) + .args(&args) + .stdout(Stdio::piped()) + .output() + .expect("zfs list -r command error"); + + let empty = String::from(""); + let data = match String::from_utf8(output.stdout) { + Ok(x) => x, + Err(e) => empty, + }; + + let mut logical_volumes: Vec> = Vec::new(); + let re: Regex = Regex::new(r"^(?:\s+)(\w*)(?:\s+)([\w-]+)(?:\s+)([\w-]*)(?:\s+)([\d\.]*)(?:\s+)([\w-]*)(?:\s+)(\d+)").unwrap(); + for line in data.lines() { + if let Some(mat) = re.captures(line) { + let mut attributes: HashMap = HashMap::new(); + attributes.insert(String::from("name"), mat[1].trim().to_string()); + attributes.insert(String::from("uuid"), mat[5].trim().to_string()); + attributes.insert(String::from("type"), "lv".to_string()); + attributes.insert(String::from("totalsize"), mat[4].to_string()); + attributes.insert(String::from("freesize"), "".to_string()); + logical_volumes.push(attributes); + } + } + return logical_volumes; +} + + +fn fill_properties(vgs: Vec>) -> Vec { + + let mut vgs_prop = Vec::new(); + + for vg in vgs.iter() { + let lvs = get_lvs(vg.get("name").unwrap().clone()); + let lvs_props = fill_properties_lv(lvs); + vgs_prop.push(serde_json::json!([ + { + "key": "type", + "value": "vg" + }, + { + "key": "name", + "value": vg.get("name") + }, + { + "key": "partitions", + "type": "list", + "value": [] + }, + { + "key": "totalsize", + "type": "integer", + "unit": "M", + "value": vg.get("totalsize") + }, + { + "key": "allocatedsize", + "type": "integer", + "unit": "M", + "value": vg.get("allocatedsize") + }, + { + "key": "freesize", + "type": "integer", + "unit": "M", + "value": vg.get("freesize") + }, + { + "key": "allocatedpercentage", + "type": "integer", + "unit": "%", + "value": vg.get("allocatedpercentage") + }, + { + "key": "health", + "value": vg.get("health") // from list: ONLINE, DEGRADED, FAULTED, OFFLINE, REMOVED, UNAVAIL + }, + { + "key": "_children", + "value": lvs_props + } + ])); + } + return vgs_prop; +} + +fn fill_properties_lv(lvs: Vec>) -> Vec { + let mut lvs_prop = Vec::new(); + + for lv in lvs.iter() { + lvs_prop.push(serde_json::json!([ + { + "key": "type", + "value": "lv" + }, + { + "key": "name", + "value": lv.get("name") + }, + { + "key": "partitions", + "type": "list", + "value": [] + }, + { + "key": "totalsize", + "type": "integer", + "unit": "M", + "value": lv.get("totalsize") + + }, + { + "key": "allocatedsize", + "type": "integer", + "unit": "M", + "value": "" + }, + { + "key": "freesize", + "type": "integer", + "unit": "M", + "value": lv.get("freesize") + }, + { + "key": "allocatedpercentage", + "type": "integer", + "unit": "%", + "value": "" + }, + { + "key": "health", + "value": "" + } + ])); + } + return lvs_prop; +} + + +// volume: zpool +// volume: dataset +// filesystem diff --git a/src/module/localinventory/data/volume/mod.rs b/src/module/localinventory/data/volume/mod.rs new file mode 100644 index 0000000..a107213 --- /dev/null +++ b/src/module/localinventory/data/volume/mod.rs @@ -0,0 +1,2 @@ +pub mod freebsd; +pub mod linux; diff --git a/src/module/localinventory/mod.rs b/src/module/localinventory/mod.rs new file mode 100644 index 0000000..14feded --- /dev/null +++ b/src/module/localinventory/mod.rs @@ -0,0 +1,3 @@ +pub mod structure; +pub mod data; +pub mod run_servers; diff --git a/src/module/localinventory/run_servers.rs b/src/module/localinventory/run_servers.rs new file mode 100644 index 0000000..807e025 --- /dev/null +++ b/src/module/localinventory/run_servers.rs @@ -0,0 +1,89 @@ +use crate::CONFIG; +use crate::module; +use std::fs::File; +use std::io::prelude::*; +use reqwest::header::USER_AGENT; + +const USER_AGENT_VALUE: &str = "FusionInventort-agent v3.0.0"; + +pub fn main() { + // get servers in config and loop + for server in CONFIG.localinventory.servers.clone().into_iter() { + if is_http(server.clone()) { + run_http(server); + } else { + run_path(server); + } + } +} + +fn is_http(server: String) -> bool { + if server.starts_with("http") { + return true; + } + return false; +} + +fn run_http(server: String) { + let inventory = module::localinventory::structure::chassis::run_inventory(); + //TODO + // do request in GET of the URL + // manage modifications of config + // do inventory + // send data on same URL but in POST + + let client = reqwest::blocking::Client::new(); + + let mut res = match client.post(server) + .header(USER_AGENT, USER_AGENT_VALUE) + .json(&inventory) + .send() { + Ok(r) => { + println!("Response: {:?} {}", r.version(), r.status()); + println!("Headers: {:#?}\n", r.headers()); + }, + Err(c) => println!("ERROR: {}", c), + }; + + + // let client = reqwest::Client::new(); + // let res = client.post(server) + // .header(USER_AGENT, USER_AGENT_VALUE) + // .json(&inventory) + // .send() + // .await; + // println!("Result: {:?}", res); + // // .await { + // // each response is wrapped in a `Result` type + // // we'll unwrap here for simplicity + + // .text() { + // // .await; + // Ok(r) => println!("hhtp return: {:?}", r), + // Err(e) => println!("http error: {:?}", e), + // }; +} + +fn run_path(server: String) { + println!("{}", server); + let inventory = module::localinventory::structure::chassis::run_inventory(); + let mut file = match File::create(server.clone() + "/testinventory.json") { + Ok(r) => r, + Err(e) => { + println!("Error writing file {}", e); + return; + } + }; + let _ = file.write_all(serde_json::to_string_pretty(&inventory).unwrap().as_bytes()); + + let mut file = match File::create(server + "/testinventory_condensed.json") { + Ok(r) => r, + Err(e) => { + println!("Error writing file {}", e); + return; + } + }; + let _ = file.write_all(serde_json::to_string(&inventory).unwrap().as_bytes()); + +} + diff --git a/src/module/localinventory/structure/chassis.rs b/src/module/localinventory/structure/chassis.rs new file mode 100644 index 0000000..f93b344 --- /dev/null +++ b/src/module/localinventory/structure/chassis.rs @@ -0,0 +1,85 @@ +use crate::module; + +// struct manufacturer { +// "key": "manufacturer", +// "value": String +// }; + +// struct chassis { +// "key": "chassis", +// "value": String +// }; + +// struct serialnumber { +// "key": "serialnumber", +// "value": String +// }; + +// struct model { +// "key": "model", +// "value": String +// }; + +// struct uuid { +// "key": "uuid", +// "value": String +// }; + +// struct Data { +// "type": String, +// properties: [ +// manufacturer, +// chassis, +// serialnumber, +// model, +// uuid +// ], +// children => [], +// connectedto => [] +// }; + +pub fn run_inventory() -> serde_json::Value { + log::info!("Get Chassis information"); + + let properties = module::localinventory::data::chassis::dmidecode::run_inventory(); + + let mut chassis = serde_json::json!({ + "type": "chassis", + "properties": properties, + "children": [], + "connectedto": [] + }); + + let mut children = Vec::new(); + + // 'FusionInventory::Inventory::Structure::MemorySlot', + + // Get CPUs + let cpus = module::localinventory::structure::cpu::run_inventory(); + children.push(cpus); + + // Get disks + let disks = module::localinventory::structure::physicaldisk::run_inventory(); + for disk in disks { + children.push(disk); + } + + // Get volumes + let volumes = module::localinventory::structure::volume::run_inventory(); + for volume in volumes { + children.push(volume); + } + + // get filesystem directly on partition, not on volumes + let filesystems = module::localinventory::structure::filesystem::run_inventory("".to_string(), "".to_string()); + for fs in filesystems { + children.push(fs); + } + + chassis["children"] = serde_json::Value::Array(children); + + log::debug!("Local inventory: {}", serde_json::to_string_pretty(&chassis).unwrap()); + return chassis; + + // TODO when finish, we can delete all temp files created on disk +} diff --git a/src/module/localinventory/structure/cpu.rs b/src/module/localinventory/structure/cpu.rs new file mode 100644 index 0000000..f21c626 --- /dev/null +++ b/src/module/localinventory/structure/cpu.rs @@ -0,0 +1,16 @@ +use crate::module; + +pub fn run_inventory() -> serde_json::Value { + log::info!("Get Chassis information"); + + let properties = module::localinventory::data::cpu::dmidecode::run_inventory(); + + let cpu = serde_json::json!({ + "type": "CPU", + "properties": properties, + "children": [], + "connectedto": [] + }); + + return cpu; +} diff --git a/src/module/localinventory/structure/filesystem.rs b/src/module/localinventory/structure/filesystem.rs new file mode 100644 index 0000000..b3b155a --- /dev/null +++ b/src/module/localinventory/structure/filesystem.rs @@ -0,0 +1,64 @@ +use crate::module; + +pub fn run_inventory(vol_type: String, vol_name: String) -> Vec { + log::info!("Get filesystem information"); + + let mut filesystems = Vec::new(); + // println!("FILESYSTEM: {:?}", vol_name); + + // return filesystems; + if vol_name == "\"zroot/ROOT/default\"" || vol_name == "\"root\"" { + let properties = serde_json::json!([ + { + "key": "name", + "value": "/" + } + ]); + + let mut children = Vec::new(); + let operatingsystem: serde_json::Value = module::localinventory::structure::operatingsystem::run_inventory(); + children.push(operatingsystem); + // partition["children"] = serde_json::Value::Array(children); + + filesystems.push(serde_json::json!({ + "type": "filesystem", + "properties": properties, + "children": children, + "connectedto": [] + })); + } else if vol_type == "".to_string() && vol_name == "".to_string() { + // Manage flesystems not on volume, but directly on partition + + let filesystemsproperties = get_data_novolume(); + + for properties in filesystemsproperties { + let mut children: Vec = Vec::new(); + for prop in properties.as_array() { + for prop2 in prop { + if prop2["key"] == "name" && prop2["value"] == "/" { + let operatingsystem: serde_json::Value = module::localinventory::structure::operatingsystem::run_inventory(); + children.push(operatingsystem); + } + } + } + filesystems.push(serde_json::json!({ + "type": "filesystem", + "properties": properties, + "children": children, + "connectedto": [] + })); + } + } + + return filesystems; +} + +#[cfg(target_os = "linux")] +fn get_data_novolume() -> Vec { + module::localinventory::data::filesystem::linux::run_inventory() +} + +#[cfg(target_os = "freebsd")] +fn get_data_novolume() -> Vec { + module::localinventory::data::filesystem::freebsd::run_inventory() +} diff --git a/src/module/localinventory/structure/mod.rs b/src/module/localinventory/structure/mod.rs new file mode 100644 index 0000000..88fc525 --- /dev/null +++ b/src/module/localinventory/structure/mod.rs @@ -0,0 +1,8 @@ +pub mod chassis; +pub mod cpu; +pub mod physicaldisk; +pub mod partition; +pub mod operatingsystem; +pub mod software; +pub mod volume; +pub mod filesystem; diff --git a/src/module/localinventory/structure/operatingsystem.rs b/src/module/localinventory/structure/operatingsystem.rs new file mode 100644 index 0000000..60ff656 --- /dev/null +++ b/src/module/localinventory/structure/operatingsystem.rs @@ -0,0 +1,56 @@ +use crate::module; + +pub fn run_inventory() -> serde_json::Value { + log::info!("Get operating system information"); + + let properties = get_data(); + + let mut ossys = serde_json::json!({ + "type": "operatingsystem", + "properties": properties, + "children": [], + "connectedto": [] + }); + + // (children) + // 'FusionInventory::Inventory::Structure::Software', + // 'FusionInventory::Inventory::Structure::RemoteManagement', + + let mut children = Vec::new(); + + let softwares = module::localinventory::structure::software::run_inventory(); + for software in softwares { + children.push(software); + } + ossys["children"] = serde_json::Value::Array(children); + + return ossys; +} + +#[cfg(target_os = "linux")] +fn get_data() -> serde_json::Value { + module::localinventory::data::operatingsystem::linux::run_inventory() +} + +#[cfg(target_os = "freebsd")] +fn get_data() -> serde_json::Value { + module::localinventory::data::operatingsystem::freebsd::run_inventory() +} + +#[cfg(target_os = "windows")] +fn get_data() { + +} + +// "windows" +// "macos" +// "ios" +// "linux" +// "android" +// "freebsd" +// "dragonfly" +// "openbsd" +// "netbsd" + + + diff --git a/src/module/localinventory/structure/partition.rs b/src/module/localinventory/structure/partition.rs new file mode 100644 index 0000000..4339526 --- /dev/null +++ b/src/module/localinventory/structure/partition.rs @@ -0,0 +1,46 @@ +use crate::module; + +pub fn run_inventory(disk: serde_json::Value) -> Vec { + log::info!("Get partitions information"); + + let mut partitions = Vec::new(); + + let partitionsproperties: Vec = get_data(disk); + + for properties in partitionsproperties { + let partition = serde_json::json!({ + "type": "partition", + "properties": properties, + "children": [], + "connectedto": [] + }); + } + return partitions; +} + +#[cfg(target_os = "linux")] +fn get_data(disk: serde_json::Value) -> Vec { + module::localinventory::data::partition::linux::run_inventory(disk) +} + +#[cfg(target_os = "freebsd")] +fn get_data(disk: serde_json::Value) -> Vec { + module::localinventory::data::partition::freebsd::run_inventory(disk) +} + +#[cfg(target_os = "windows")] +fn get_data(disk: serde_json::Value) -> Vec { + // module::localinventory::data::partition::windows::run_inventory(disk) + let data = Vec::new(); + return data; +} + +// "windows" +// "macos" +// "ios" +// "linux" +// "android"// +// "freebsd" +// "dragonfly" +// "openbsd" +// "netbsd" diff --git a/src/module/localinventory/structure/physicaldisk.rs b/src/module/localinventory/structure/physicaldisk.rs new file mode 100644 index 0000000..32a6dc2 --- /dev/null +++ b/src/module/localinventory/structure/physicaldisk.rs @@ -0,0 +1,54 @@ +use crate::module; + +pub fn run_inventory() -> Vec { + log::info!("Get physicaldisks information"); + + let mut disks = Vec::new(); + + let disksproperties: Vec = get_data(); + for properties in disksproperties { + let mut mydisk = serde_json::json!({ + "type": "physicaldisk", + "properties": properties, + "children": [], + "connectedto": [] + }); + + let mut children = Vec::new(); + + let partitions = module::localinventory::structure::partition::run_inventory(properties); + for partition in partitions { + children.push(partition); + } + + mydisk["children"] = serde_json::Value::Array(children); + + disks.push(mydisk); + } + return disks; +} + +#[cfg(target_os = "linux")] +fn get_data() -> Vec { + module::localinventory::data::physicaldisk::linux::run_inventory() +} + +#[cfg(target_os = "freebsd")] +fn get_data() -> Vec { + module::localinventory::data::physicaldisk::freebsd::run_inventory() +} + +#[cfg(target_os = "windows")] +fn get_data() { + +} + +// "windows" +// "macos" +// "ios" +// "linux" +// "android" +// "freebsd" +// "dragonfly" +// "openbsd" +// "netbsd" diff --git a/src/module/localinventory/structure/software.rs b/src/module/localinventory/structure/software.rs new file mode 100644 index 0000000..5b12af8 --- /dev/null +++ b/src/module/localinventory/structure/software.rs @@ -0,0 +1,33 @@ +use crate::module; + +pub fn run_inventory() -> Vec { + log::info!("Get softwares information"); + + let mut softwares = Vec::new(); + + let softwareproperties = get_data(); + + for properties in softwareproperties { + let software = serde_json::json!({ + "type": "software", + "properties": properties, + "children": [], + "connectedto": [] + }); + + softwares.push(software); + } + // List of packages tools + // https://github.com/sigoden/upt + return softwares; +} + +#[cfg(target_os = "linux")] +fn get_data() -> Vec { + module::localinventory::data::software::linux::run_inventory() +} + +#[cfg(target_os = "freebsd")] +fn get_data() -> Vec { + module::localinventory::data::software::freebsd::run_inventory() +} diff --git a/src/module/localinventory/structure/volume.rs b/src/module/localinventory/structure/volume.rs new file mode 100644 index 0000000..66dc133 --- /dev/null +++ b/src/module/localinventory/structure/volume.rs @@ -0,0 +1,112 @@ +use crate::module; + +pub fn run_inventory() -> Vec { + log::info!("Get volumes information"); + + let volumesproperties: Vec = get_data(); + + let data = loop_properties(volumesproperties); + + // for properties in volumesproperties { + // // new_properties = properties.clone(); + // // TODO delete children + // for prop in properties.as_array() { + // for prop2 in prop { + // if prop2["key"] == "_children" { + // println!("CHILDREN HERE !"); + + // } else { + // println!("TEST my prop ****: {:?}", prop2); + // } + // } + // } + // let mut myvolume = serde_json::json!({ + // "type": "volume", + // "properties": properties, + // "children": [], + // "connectedto": [] + // }); + + // let mut children = Vec::new(); + + // // let partitions = module::localinventory::structure::partition::run_inventory(properties); + // // for partition in partitions { + // // children.push(partition); + // // } + + // myvolume["children"] = serde_json::Value::Array(children); + + // volumes.push(myvolume); + // } + // return volumes; + return data; + +} + +fn loop_properties(volumesproperties: Vec) -> Vec { + let mut volumes = Vec::new(); + + for volume_properties in volumesproperties { + let mut fill_properties: Vec = Vec::new(); + let mut children: Vec = Vec::new(); + let mut vol_type = "".to_string(); + let mut vol_name = "".to_string(); + for prop in volume_properties.as_array() { + for prop2 in prop { + if prop2["key"] == "_children" { + children = loop_properties(prop2["value"].as_array().unwrap().clone()); + } else { + fill_properties.push(prop2.clone()); + if prop2["key"] == "type" { + vol_type = prop2["value"].to_string(); + } + if prop2["key"] == "name" { + vol_name = prop2["value"].to_string(); + } + } + } + } + if children.iter().count() == 0 { + // TODO get filesystems + let filesystem = module::localinventory::structure::filesystem::run_inventory(vol_type, vol_name); + if filesystem.iter().count() > 0 { + children.push(filesystem.into_iter().next().unwrap()); + } + } + + volumes.push(serde_json::json!({ + "type": "volume", + "properties": fill_properties, + "children": children, + "connectedto": [] + })); + } + return volumes; +} + +#[cfg(target_os = "linux")] +fn get_data() -> Vec { + module::localinventory::data::volume::linux::run_inventory() +} + +#[cfg(target_os = "freebsd")] +fn get_data() -> Vec { + module::localinventory::data::volume::freebsd::run_inventory() +} + +#[cfg(target_os = "windows")] +fn get_data() -> Vec { + // module::localinventory::data::partition::windows::run_inventory(disk) + let data = Vec::new(); + return data; +} + +// "windows" +// "macos" +// "ios" +// "linux" +// "android"// +// "freebsd" +// "dragonfly" +// "openbsd" +// "netbsd" diff --git a/src/module/mod.rs b/src/module/mod.rs new file mode 100644 index 0000000..b6b53c3 --- /dev/null +++ b/src/module/mod.rs @@ -0,0 +1,2 @@ +pub mod localinventory; +pub mod common; diff --git a/src/static/webserver/css/main.css b/src/static/webserver/css/main.css new file mode 100644 index 0000000..0638ab3 --- /dev/null +++ b/src/static/webserver/css/main.css @@ -0,0 +1,9 @@ +.logo { + margin-top: 10px; + margin-bottom: 50px; + margin-left: 10px; +} + +td { + width: 250px; +} diff --git a/src/static/webserver/img/favicon.ico b/src/static/webserver/img/favicon.ico new file mode 100644 index 0000000..b394762 Binary files /dev/null and b/src/static/webserver/img/favicon.ico differ diff --git a/src/static/webserver/img/logo_fusioninventory.png b/src/static/webserver/img/logo_fusioninventory.png new file mode 100644 index 0000000..71a3089 Binary files /dev/null and b/src/static/webserver/img/logo_fusioninventory.png differ