rufish 0.4.0

An asynchronous Redfish client library for BMC/server management in Rust.
Documentation
//! Enriched high-level APIs that aggregate multiple Redfish resources
//! into UI-friendly summary structures.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Overall health status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HealthStatus {
    OK,
    Warning,
    Critical,
    Unknown,
}

impl HealthStatus {
    pub fn from_str_opt(s: Option<&str>) -> Self {
        match s {
            Some("OK") => Self::OK,
            Some("Warning") => Self::Warning,
            Some("Critical") => Self::Critical,
            _ => Self::Unknown,
        }
    }

    /// Returns the worse of two statuses.
    pub fn worse(self, other: Self) -> Self {
        match (&self, &other) {
            (Self::Critical, _) | (_, Self::Critical) => Self::Critical,
            (Self::Warning, _) | (_, Self::Warning) => Self::Warning,
            (Self::OK, _) | (_, Self::OK) => Self::OK,
            _ => Self::Unknown,
        }
    }
}

// --- System Health Summary ---

/// Aggregated system health from multiple resources.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemHealth {
    pub overall: HealthStatus,
    pub processors: HealthStatus,
    pub memory: HealthStatus,
    pub storage: HealthStatus,
    pub temperatures: Vec<SensorHealth>,
    pub fans: Vec<SensorHealth>,
    pub power_supplies: Vec<PsuHealth>,
}

/// A sensor reading with health context.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensorHealth {
    pub name: String,
    pub reading: Option<f64>,
    pub units: Option<String>,
    pub status: HealthStatus,
    pub upper_warning: Option<f64>,
    pub upper_critical: Option<f64>,
}

/// Power supply health.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PsuHealth {
    pub name: String,
    pub model: Option<String>,
    pub capacity_watts: Option<f64>,
    pub status: HealthStatus,
}

// --- Enriched Log Entries ---

/// A log entry with message registry translation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrichedLogEntry {
    pub id: String,
    pub timestamp: Option<String>,
    pub severity: Option<String>,
    pub raw_message_id: Option<String>,
    pub message: String,
    pub translated_message: Option<String>,
    pub resolution: Option<String>,
}

// --- Storage Overview ---

/// Aggregated storage topology.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageOverview {
    pub controllers: Vec<ControllerSummary>,
    pub volumes: Vec<VolumeSummary>,
    pub unassigned_drives: Vec<DriveSummary>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControllerSummary {
    pub name: String,
    pub manufacturer: Option<String>,
    pub model: Option<String>,
    pub firmware_version: Option<String>,
    pub status: HealthStatus,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumeSummary {
    pub id: String,
    pub name: String,
    pub capacity_bytes: Option<u64>,
    pub raid_type: Option<String>,
    pub status: HealthStatus,
    pub drive_count: usize,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriveSummary {
    pub id: String,
    pub name: String,
    pub manufacturer: Option<String>,
    pub model: Option<String>,
    pub capacity_bytes: Option<u64>,
    pub media_type: Option<String>,
    pub protocol: Option<String>,
    pub status: HealthStatus,
}

// --- Account helpers ---

/// Simplified user account info.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAccount {
    pub id: String,
    pub username: String,
    pub role: Option<String>,
    pub enabled: bool,
    pub locked: bool,
}

// --- Event subscription helper ---

/// Simplified event subscription creation parameters.
#[derive(Debug, Clone)]
pub struct SubscribeParams<'a> {
    pub destination: &'a str,
    pub event_types: Option<&'a [&'a str]>,
    pub severity_filter: Option<&'a str>,
    pub context: Option<&'a str>,
}

// --- Parsing helpers (from raw JSON) ---

pub(crate) fn parse_system_health(
    system: &Value,
    thermal: Option<&Value>,
    power: Option<&Value>,
) -> SystemHealth {
    let proc_health = system
        .pointer("/ProcessorSummary/Status/HealthRollup")
        .or_else(|| system.pointer("/ProcessorSummary/Status/Health"))
        .and_then(|v| v.as_str());
    let mem_health = system
        .pointer("/MemorySummary/Status/HealthRollup")
        .or_else(|| system.pointer("/MemorySummary/Status/Health"))
        .and_then(|v| v.as_str());
    let storage_health = system
        .pointer("/Status/HealthRollup")
        .and_then(|v| v.as_str());

    let temperatures: Vec<SensorHealth> = thermal
        .and_then(|t| t.get("Temperatures"))
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|t| {
                    let name = t.get("Name").and_then(|v| v.as_str())?;
                    Some(SensorHealth {
                        name: name.to_string(),
                        reading: t.get("ReadingCelsius").and_then(|v| v.as_f64()),
                        units: Some("°C".to_string()),
                        status: HealthStatus::from_str_opt(
                            t.pointer("/Status/Health").and_then(|v| v.as_str()),
                        ),
                        upper_warning: t.get("UpperThresholdNonCritical").and_then(|v| v.as_f64()),
                        upper_critical: t.get("UpperThresholdCritical").and_then(|v| v.as_f64()),
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    let fans: Vec<SensorHealth> = thermal
        .and_then(|t| t.get("Fans"))
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|f| {
                    let name = f.get("Name").and_then(|v| v.as_str())?;
                    Some(SensorHealth {
                        name: name.to_string(),
                        reading: f.get("Reading").and_then(|v| v.as_f64()),
                        units: f.get("ReadingUnits").and_then(|v| v.as_str()).map(String::from),
                        status: HealthStatus::from_str_opt(
                            f.pointer("/Status/Health").and_then(|v| v.as_str()),
                        ),
                        upper_warning: None,
                        upper_critical: None,
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    let power_supplies: Vec<PsuHealth> = power
        .and_then(|p| p.get("PowerSupplies"))
        .and_then(|v| v.as_array())
        .map(|arr| {
            arr.iter()
                .filter_map(|ps| {
                    let name = ps.get("Name").and_then(|v| v.as_str())?;
                    Some(PsuHealth {
                        name: name.to_string(),
                        model: ps.get("Model").and_then(|v| v.as_str()).map(String::from),
                        capacity_watts: ps.get("PowerCapacityWatts").and_then(|v| v.as_f64()),
                        status: HealthStatus::from_str_opt(
                            ps.pointer("/Status/Health").and_then(|v| v.as_str()),
                        ),
                    })
                })
                .collect()
        })
        .unwrap_or_default();

    let mut overall = HealthStatus::from_str_opt(proc_health);
    overall = overall.worse(HealthStatus::from_str_opt(mem_health));
    for t in temperatures.iter() {
        let s: HealthStatus = t.status.clone();
        overall = overall.worse(s);
    }
    for f in fans.iter() {
        let s: HealthStatus = f.status.clone();
        overall = overall.worse(s);
    }
    for ps in power_supplies.iter() {
        let s: HealthStatus = ps.status.clone();
        overall = overall.worse(s);
    }

    SystemHealth {
        overall,
        processors: HealthStatus::from_str_opt(proc_health),
        memory: HealthStatus::from_str_opt(mem_health),
        storage: HealthStatus::from_str_opt(storage_health),
        temperatures,
        fans,
        power_supplies,
    }
}

pub(crate) fn parse_enriched_log_entry(
    entry: &Value,
    messages: &std::collections::HashMap<String, (String, Option<String>)>,
) -> Option<EnrichedLogEntry> {
    let id = entry.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string();
    let message_id = entry.get("MessageId").and_then(|v| v.as_str()).map(String::from);
    let raw_message = entry
        .get("Message")
        .and_then(|v| v.as_str())
        .unwrap_or("")
        .to_string();

    let (translated, resolution) = message_id
        .as_deref()
        .and_then(|mid| {
            // Try full ID, then just the suffix after the last dot
            messages.get(mid).or_else(|| {
                mid.rsplit_once('.').and_then(|(_, suffix)| messages.get(suffix))
            })
        })
        .map(|(msg, res)| (Some(msg.clone()), res.clone()))
        .unwrap_or((None, None));

    Some(EnrichedLogEntry {
        id,
        timestamp: entry.get("Created").and_then(|v| v.as_str()).map(String::from),
        severity: entry.get("Severity").and_then(|v| v.as_str()).map(String::from),
        raw_message_id: message_id,
        message: raw_message,
        translated_message: translated,
        resolution,
    })
}