Files
glances-rs/crates/glances-plugins/src/plugins/ip.rs
T
glances-rs cb22e53aa6 M3: extended plugins
- folders, amps, ports, raid, wifi, ip (drop-in compatible stats)
- differential tests pass for all 22 implemented plugins
- TUI left sidebar extended
2026-10-09 23:47:21 +00:00

182 lines
6.1 KiB
Rust

//! IP plugin: private and (optionally) public IP information.
use crate::fields;
use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext};
use glances_core::{Args, Config};
use serde_json::{Value, json};
use std::time::Instant;
pub struct IpPlugin {
pub model: PluginModel,
public_api: Option<String>,
public_field: String,
public_template: String,
public_disabled: bool,
public_username: Option<String>,
public_password: Option<String>,
public_interval: f64,
public_timer: Instant,
public_address: String,
public_info: Option<Value>,
}
impl IpPlugin {
pub fn new(config: &Config, args: &Args) -> Self {
let fields = fields! {
"address" => Field::new("Private IP address"),
"mask" => Field::new("Private IP mask"),
"mask_cidr" => Field::new("Private IP mask in CIDR format").unit("number"),
"gateway" => Field::new("Private IP gateway"),
"public_address" => Field::new("Public IP address"),
"public_info_human" => Field::new("Public IP information"),
};
let model = PluginModel::new(
"ip",
fields,
Vec::new(),
json!({}),
config,
args.time.unwrap_or(2.0),
args.disable_history,
args.disable_config_exec,
);
let public_api = model.get_conf_value("public_api", "").first().cloned();
let public_disabled = model
.get_conf_value("public_disabled", "")
.first()
.map(|v| v.to_lowercase() != "false")
.unwrap_or(true)
|| public_api.is_none();
Self {
model,
public_api,
public_field: "ip".to_string(),
public_template: String::new(),
public_disabled,
public_username: None,
public_password: None,
public_interval: 300.0,
public_timer: Instant::now() - std::time::Duration::from_secs(3600),
public_address: String::new(),
public_info: None,
}
}
fn refresh_public(&mut self) {
if self.public_disabled {
return;
}
if self.public_timer.elapsed().as_secs_f64() < self.public_interval {
return;
}
self.public_timer = Instant::now();
let Some(api) = &self.public_api else {
return;
};
let mut request = ureq::get(api)
.config()
.timeout_global(Some(std::time::Duration::from_secs(2)))
.build();
if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) {
request = request.header("Authorization", &format!("Basic {}:{}", username, password));
}
if let Ok(mut response) = request.call() {
if let Ok(value) = response.body_mut().read_json::<Value>() {
self.public_address =
value.get(&self.public_field).and_then(Value::as_str).unwrap_or("").to_string();
self.public_info = Some(value);
}
}
}
fn public_info_human(&self) -> String {
let Some(info) = &self.public_info else {
return String::new();
};
if self.public_template.is_empty() {
return String::new();
}
let mut result = self.public_template.clone();
for (key, value) in info.as_object().into_iter().flatten() {
let text = match value {
Value::String(s) => s.clone(),
other => glances_core::py_str(other),
};
result = result.replace(&format!("{{{key}}}"), &text);
}
result
}
}
impl Plugin for IpPlugin {
fn name(&self) -> &str {
"ip"
}
fn model(&self) -> &PluginModel {
&self.model
}
fn model_mut(&mut self) -> &mut PluginModel {
&mut self.model
}
fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result<Value> {
let mut stats = serde_json::Map::new();
let (address, mask) = crate::collectors::get_ip_address();
if let Some(address) = &address {
stats.insert("address".to_string(), json!(address));
}
if let Some(mask) = &mask {
stats.insert("mask".to_string(), json!(mask));
}
stats.insert("mask_cidr".to_string(), json!(crate::collectors::ip_to_cidr(mask.as_deref())));
self.refresh_public();
if !self.public_address.is_empty() {
let public = if ctx.args.hide_public_info {
mask_public_ip(&self.public_address)
} else {
self.public_address.clone()
};
stats.insert("public_address".to_string(), json!(public));
let human = self.public_info_human();
if !human.is_empty() {
stats.insert("public_info_human".to_string(), json!(human));
}
}
Ok(Value::Object(stats))
}
fn display_curse(&self) -> bool {
true
}
fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec<glances_core::StatLine> {
let mut ret = Vec::new();
let s = &self.model.stats;
let Some(address) = s.get("address").and_then(Value::as_str) else {
return ret;
};
let mut text = address.to_string();
if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) {
if mask_cidr > 0 {
text += &format!("/{mask_cidr}");
}
}
ret.push(self.model.curse_add_line(text, glances_core::Decoration::Default, false, false, false));
if let Some(gateway) = s.get("gateway").and_then(Value::as_str) {
ret.push(self.model.curse_add_line(format!(" gw {gateway}"), glances_core::Decoration::Default, true, false, false));
}
ret
}
}
fn mask_public_ip(address: &str) -> String {
let parts: Vec<&str> = address.split('.').collect();
if parts.len() == 4 {
format!("{}.{}.{}.xxx", parts[0], parts[1], parts[2])
} else {
"xxx".to_string()
}
}