Skip to content

Commit

Permalink
Add disks and processor name information to the monitoring page
Browse files Browse the repository at this point in the history
  • Loading branch information
dormant-user committed Sep 22, 2024
1 parent 3bac8b6 commit 576cba7
Show file tree
Hide file tree
Showing 13 changed files with 408 additions and 101 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ reqwest = { version = "0.11", features = ["blocking", "json"] }
minijinja = { version = "2.3.1", features = ["loader"] }
url = "2.5.2"
regex = "1.10.6"
Inflector = "0.11"
openssl = "0.10.64"
dotenv = "0.15.0"
futures-util = "0.3.30"
Expand Down
9 changes: 8 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod routes;
/// Module to store all the helper functions.
mod squire;
mod templates;
mod system_info;
mod resources;

/// Contains entrypoint and initializer settings to trigger the asynchronous `HTTPServer`
///
Expand Down Expand Up @@ -78,4 +78,11 @@ pub async fn start() -> io::Result<()> {
server.bind(host)?
.run()
.await
// match server.bind(host) {
// Ok(bound_server) => bound_server.run().await,
// Err(err) => {
// log::error!("Failed to bind server: {}", err);
// Err(err)
// }
// }
}
153 changes: 153 additions & 0 deletions src/resources/disks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
use std::collections::HashMap;
use std::process::Command;
use std::str;
use regex::Regex;
use serde_json::Value;

// todo: tested only on macOS

fn format_nos(input: f64) -> f64 {
if input.fract() == 0.0 {
input.trunc()
} else {
input
}
}

fn size_converter(byte_size: f64) -> String {
let size_name = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
let index = (byte_size.log(1024.0)).floor() as usize;
format!("{:.2} {}", format_nos(byte_size / 1024.0_f64.powi(index as i32)), size_name[index])
}

fn parse_size(size_str: &str) -> String {
let re = Regex::new(r"([\d.]+)([KMGTP]?)").unwrap();
if let Some(caps) = re.captures(size_str.trim()) {
let value: f64 = caps[1].parse().unwrap();
let unit = &caps[2];
let unit_multipliers = HashMap::from([
("K", 2_f64.powi(10)),
("M", 2_f64.powi(20)),
("G", 2_f64.powi(30)),
("T", 2_f64.powi(40)),
("P", 2_f64.powi(50)),
]);
let multiplier = unit_multipliers.get(unit).unwrap_or(&1.0);
return size_converter(value * multiplier);
}
size_str.replace("K", " KB")
.replace("M", " MB")
.replace("G", " GB")
.replace("T", " TB")
.replace("P", " PB")
}

fn run_command(command: &str, args: &[&str]) -> String {
let output = Command::new(command)
.args(args)
.output()
.expect("Failed to execute command");
String::from_utf8_lossy(&output.stdout).to_string()
}

fn is_physical_disk(lib_path: &str, device_id: &str) -> bool {
let output = run_command(lib_path, &["info", device_id]);
// !output.contains("Virtual:Yes")
for line in output.split("\n") {
if line.contains("Virtual:") && line.contains("Yes") {
return false;
}
}
true
}

fn linux_disks(lib_path: &str) -> Vec<HashMap<String, String>> {
let output = run_command(lib_path, &["-o", "NAME,SIZE,TYPE,MODEL", "-d"]);
let disks: Vec<&str> = output.lines().collect();
let filtered_disks: Vec<&str> = disks.into_iter().filter(|&disk| !disk.contains("loop")).collect();
let keys_raw = filtered_disks[0].to_lowercase()
.replace("name", "DeviceID")
.replace("model", "Name")
.replace("size", "Size");
let keys: Vec<&str> = keys_raw.split_whitespace().collect();
let mut disk_info = Vec::new();
for line in &filtered_disks[1..] {
let values: Vec<&str> = line.split_whitespace().collect();
let mut disk_map = HashMap::new();
for (key, value) in keys.iter().zip(values.iter()) {
disk_map.insert(key.to_string(), value.to_string());
}
disk_map.insert("Size".to_string(), parse_size(disk_map.get("Size").unwrap()));
disk_map.remove("type");
disk_info.push(disk_map);
}
disk_info
}

fn darwin_disks(lib_path: &str) -> Vec<HashMap<String, String>> {
let output = run_command(lib_path, &["list"]);
let disks: Vec<&str> = output.lines().collect();
let disk_lines: Vec<&str> = disks.into_iter().filter(|&line| line.starts_with("/dev/disk")).collect();
let mut disk_info = Vec::new();
for line in disk_lines {
let device_id = line.split_whitespace().next().unwrap();
if !is_physical_disk(lib_path, device_id) {
continue;
}
let disk_info_output = run_command(lib_path, &["info", device_id]);
let info_lines: Vec<&str> = disk_info_output.lines().collect();
let mut disk_data = HashMap::new();
for info_line in info_lines {
if info_line.contains("Device / Media Name:") {
disk_data.insert("Name".to_string(), info_line.split(":").nth(1).unwrap().trim().to_string());
}
if info_line.contains("Disk Size:") {
let size_info = info_line.split(":").nth(1).unwrap().split("(").next().unwrap().trim().to_string();
disk_data.insert("Size".to_string(), size_info);
}
}
disk_data.insert("DeviceID".to_string(), device_id.to_string());
disk_info.push(disk_data);
}
disk_info
}

fn reformat_windows(data: &mut HashMap<String, Value>) -> HashMap<String, String> {
let size = data.get("Size").unwrap().as_f64().unwrap();
let model = data.get("Model").unwrap().as_str().unwrap().to_string();
let mut reformatted_data = HashMap::new();
reformatted_data.insert("Size".to_string(), size_converter(size));
reformatted_data.insert("Name".to_string(), model);
reformatted_data.insert("DeviceID".to_string(), data.get("DeviceID").unwrap().as_str().unwrap().to_string());
reformatted_data
}

fn windows_disks(lib_path: &str) -> Vec<HashMap<String, String>> {
let ps_command = "Get-CimInstance Win32_DiskDrive | Select-Object Caption, DeviceID, Model, Partitions, Size | ConvertTo-Json";
let output = run_command(lib_path, &["-Command", ps_command]);
let disks_info: Value = serde_json::from_str(&output).unwrap();
let mut disk_info = Vec::new();
if let Some(disks) = disks_info.as_array() {
for disk in disks {
let mut disk_map: HashMap<String, Value> = serde_json::from_value(disk.clone()).unwrap();
disk_info.push(reformat_windows(&mut disk_map));
}
} else {
let mut disk_map: HashMap<String, Value> = serde_json::from_value(disks_info).unwrap();
disk_info.push(reformat_windows(&mut disk_map));
}
disk_info
}

pub fn get_all_disks() -> Vec<HashMap<String, String>> {
let operating_system = std::env::consts::OS;
match operating_system {
"windows" => windows_disks("C:\\Program Files\\PowerShell\\7\\pwsh.exe"),
"macos" => darwin_disks("/usr/sbin/diskutil"),
"linux" => linux_disks("/usr/bin/lsblk"),
_ => {
log::error!("Unsupported operating system");
Vec::new()
}
}
}
44 changes: 44 additions & 0 deletions src/resources/info.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use std::collections::HashMap;
use chrono::Utc;
use sysinfo::{DiskExt, System, SystemExt};
use crate::{squire, resources};

/// Function to get system information
///
/// This function retrieves system information such as basic system information and memory/storage information.
///
/// # Returns
///
/// A tuple containing the `SystemInfoBasic` and `SystemInfoMemStorage` structs.
pub fn get_sys_info() -> HashMap<&'static str, HashMap<&'static str, String>> {
let mut sys = System::new_all();
sys.refresh_all();

// Uptime
let boot_time = sys.boot_time();
let uptime_duration = Utc::now().timestamp() - boot_time as i64;
let uptime = squire::util::convert_seconds(uptime_duration);

let total_memory = squire::util::size_converter(sys.total_memory()); // in bytes
let total_swap = squire::util::size_converter(sys.total_swap()); // in bytes
let total_storage = squire::util::size_converter(sys.disks().iter().map(|disk| disk.total_space()).sum::<u64>());

// Basic and Memory/Storage Info
let os_arch = resources::system::os_arch();
let basic = HashMap::from_iter(vec![
("node", sys.host_name().unwrap_or("Unknown".to_string())),
("system", os_arch.name),
("architecture", os_arch.architecture),
("uptime", uptime),
("CPU_cores_raw", sys.cpus().len().to_string()
)]);
let mem_storage = HashMap::from_iter(vec![
("memory", total_memory),
("swap", total_swap),
("storage", total_storage)
]);
HashMap::from_iter(vec![
("basic", basic),
("mem_storage", mem_storage)
])
}
8 changes: 8 additions & 0 deletions src/resources/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// This module contains all the system information related functions.
pub mod info;
/// This module contains all the network related functions.
pub mod network;
/// This module contains processor related functions.
pub mod processor;
pub mod disks;
pub mod system;
19 changes: 8 additions & 11 deletions src/system_info/network.rs → src/resources/network.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use reqwest;
use serde::{Deserialize, Serialize};
use std::net::UdpSocket;
Expand Down Expand Up @@ -74,22 +75,18 @@ fn private_ip_address() -> Option<String> {
/// * `public_ip_address` - The public IP address of the system
#[derive(Serialize, Deserialize, Debug)]
pub struct SystemInfoNetwork {
private_ip_address: String,
public_ip_address: String,
private_ip_address_raw: String,
public_ip_address_raw: String,
}

/// Function to get network information
///
/// This function retrieves the private and public IP addresses of the system.
///
/// # Returns
///
/// A `SystemInfoNetwork` struct containing the private and public IP addresses.
pub async fn get_network_info() -> SystemInfoNetwork {
pub async fn get_network_info() -> HashMap<&'static str, String> {
let private_ip = private_ip_address().unwrap_or_default();
let public_ip = public_ip_address().await.unwrap_or_default();
SystemInfoNetwork {
private_ip_address: private_ip,
public_ip_address: public_ip,
}
HashMap::from([
("Private_IP_Address_raw", private_ip),
("Public_IP_Address_raw", public_ip),
])
}
75 changes: 75 additions & 0 deletions src/resources/processor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use crate::resources;
use std::fs::File;
use std::io::{self, BufRead};
use std::process::Command;

fn get_processor_info_darwin(lib_path: &str) -> Result<String, &'static str> {
let output = match Command::new(lib_path)
.arg("-n")
.arg("machdep.cpu.brand_string")
.output()
{
Ok(output) => output,
Err(_) => return Err("Failed to execute command"),
};
let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(result)
}
fn get_processor_info_linux(lib_path: &str) -> Result<String, &'static str> {
let file = match File::open(lib_path) {
Ok(file) => file,
Err(_) => return Err("Failed to open file"),
};
for line in io::BufReader::new(file).lines() {
match line {
Ok(line) => {
if line.contains("model name") {
let parts: Vec<&str> = line.split(':').collect();
if parts.len() == 2 {
return Ok(parts[1].trim().to_string());
}
}
}
Err(_) => return Err("Error reading line"),
}
}
Err("Model name not found")
}
fn get_processor_info_windows(lib_path: &str) -> Result<String, &'static str> {
let output = match Command::new(lib_path)
.arg("cpu")
.arg("get")
.arg("name")
.output()
{
Ok(output) => output,
Err(_) => return Err("Failed to execute command"),
};
let output_str = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = output_str.trim().split('\n').collect();
if lines.len() > 1 {
Ok(lines[1].trim().to_string())
} else {
Err("Invalid output from command")
}
}

pub fn get_name() -> Option<String> {
let os = resources::system::os_arch().name;
let result = match os.as_str() {
"darwin" => get_processor_info_darwin("/usr/sbin/sysctl"),
"linux" => get_processor_info_linux("/proc/cpuinfo"),
"windows" => get_processor_info_windows("C:\\Windows\\System32\\wbem\\wmic.exe"),
_ => {
log::error!("Unsupported operating system: {}", os);
Err("Unsupported operating system")
},
};
match result {
Ok(info) => Some(info),
Err(err) => {
log::error!("{}", err);
None
}
}
}
Loading

0 comments on commit 576cba7

Please sign in to comment.