use log::debug;
use reqwest::{Client, Response, StatusCode};
use serde_json::Value;
use crate::bios_config::{BiosConfig, BiosConfigSource};
use crate::enriched::{
self, ControllerSummary, DriveSummary, EnrichedLogEntry, HealthStatus, StorageOverview,
SubscribeParams, SystemHealth, UserAccount, VolumeSummary,
};
use crate::error::{RedfishError, Result};
use crate::registry::{
self, RegistryAttribute, RegistryInfo, RegistryMessage,
};
use crate::sse::EventStream;
use crate::topology::{
ChassisNode, ComponentNode, HardwareTopology, ManagerNode, SystemNode,
};
use crate::types::*;
pub struct RedfishClientBuilder {
host: String,
username: Option<String>,
password: Option<String>,
client: Option<Client>,
session_token: Option<String>,
session_uri: Option<String>,
}
impl RedfishClientBuilder {
pub fn credentials(mut self, username: &str, password: &str) -> Self {
self.username = Some(username.to_string());
self.password = Some(password.to_string());
self
}
pub fn client(mut self, client: Client) -> Self {
self.client = Some(client);
self
}
pub fn session(mut self, token: &str, session_uri: &str) -> Self {
self.session_token = Some(token.to_string());
self.session_uri = Some(session_uri.to_string());
self
}
pub fn build(self) -> Result<RedfishClient> {
let base_url = if self.host.starts_with("http") {
self.host.trim_end_matches('/').to_string()
} else {
format!("https://{}", self.host.trim_end_matches('/'))
};
let client = match self.client {
Some(c) => c,
None => Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(30))
.build()?,
};
Ok(RedfishClient {
base_url,
username: self.username.unwrap_or_default(),
password: self.password.unwrap_or_default(),
client,
session_token: self.session_token,
session_uri: self.session_uri,
})
}
}
pub struct RedfishClient {
base_url: String,
username: String,
password: String,
client: Client,
session_token: Option<String>,
session_uri: Option<String>,
}
impl RedfishClient {
pub fn builder(host: &str) -> RedfishClientBuilder {
RedfishClientBuilder {
host: host.to_string(),
username: None,
password: None,
client: None,
session_token: None,
session_uri: None,
}
}
pub fn new(host: &str, username: &str, password: &str) -> Result<Self> {
Self::builder(host).credentials(username, password).build()
}
pub fn set_session(&mut self, token: &str, session_uri: &str) {
self.session_token = Some(token.to_string());
self.session_uri = Some(session_uri.to_string());
}
pub fn session_token(&self) -> Option<&str> {
self.session_token.as_deref()
}
pub fn session_uri(&self) -> Option<&str> {
self.session_uri.as_deref()
}
pub async fn login(&mut self) -> Result<()> {
let url = format!("{}/redfish/v1/SessionService/Sessions", self.base_url);
let body = SessionCreate {
user_name: self.username.clone(),
password: self.password.clone(),
};
let resp = self.client.post(&url).json(&body).send().await?;
if resp.status() == StatusCode::CREATED || resp.status() == StatusCode::OK {
let token = resp
.headers()
.get("X-Auth-Token")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
.ok_or(RedfishError::AuthFailed)?;
let location = resp
.headers()
.get("Location")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
self.session_token = Some(token);
self.session_uri = location;
debug!("Session established");
Ok(())
} else {
Err(RedfishError::AuthFailed)
}
}
pub async fn logout(&mut self) -> Result<()> {
if let (Some(token), Some(uri)) = (&self.session_token, &self.session_uri) {
let url = if uri.starts_with("http") {
uri.clone()
} else {
format!("{}{}", self.base_url, uri)
};
let _ = self.client
.delete(&url)
.header("X-Auth-Token", token)
.send()
.await;
}
self.session_token = None;
self.session_uri = None;
Ok(())
}
pub async fn get(&self, path: &str) -> Result<Value> {
let url = if path.starts_with("http") {
path.to_string()
} else {
format!("{}{}", self.base_url, path)
};
let resp = self.auth_get(&url).await?;
self.handle_response(resp).await
}
pub async fn get_as<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
let val = self.get(path).await?;
serde_json::from_value(val).map_err(|e| RedfishError::Parse(e.to_string()))
}
pub async fn post(&self, path: &str, body: &Value) -> Result<Value> {
let url = if path.starts_with("http") {
path.to_string()
} else {
format!("{}{}", self.base_url, path)
};
let mut req = self.client.post(&url).json(body);
if let Some(token) = &self.session_token {
req = req.header("X-Auth-Token", token);
} else {
req = req.basic_auth(&self.username, Some(&self.password));
}
let resp = req.send().await?;
self.handle_response(resp).await
}
pub async fn patch(&self, path: &str, body: &Value) -> Result<Value> {
let url = if path.starts_with("http") {
path.to_string()
} else {
format!("{}{}", self.base_url, path)
};
let mut req = self.client.patch(&url).json(body);
if let Some(token) = &self.session_token {
req = req.header("X-Auth-Token", token);
} else {
req = req.basic_auth(&self.username, Some(&self.password));
}
let resp = req.send().await?;
self.handle_response(resp).await
}
pub async fn delete(&self, path: &str) -> Result<()> {
let url = if path.starts_with("http") {
path.to_string()
} else {
format!("{}{}", self.base_url, path)
};
let mut req = self.client.delete(&url);
if let Some(token) = &self.session_token {
req = req.header("X-Auth-Token", token);
} else {
req = req.basic_auth(&self.username, Some(&self.password));
}
let resp = req.send().await?;
if resp.status().is_success() || resp.status() == StatusCode::NO_CONTENT {
Ok(())
} else {
let status = resp.status().as_u16();
let text = resp.text().await.unwrap_or_default();
Err(RedfishError::Api { status, message: text })
}
}
pub async fn get_service_root(&self) -> Result<ServiceRoot> {
self.get_as("/redfish/v1/").await
}
pub async fn list_systems(&self) -> Result<Collection> {
self.get_as("/redfish/v1/Systems").await
}
pub async fn get_system(&self, id: &str) -> Result<ComputerSystem> {
self.get_as(&format!("/redfish/v1/Systems/{}", id)).await
}
pub async fn list_chassis(&self) -> Result<Collection> {
self.get_as("/redfish/v1/Chassis").await
}
pub async fn get_chassis(&self, id: &str) -> Result<Chassis> {
self.get_as(&format!("/redfish/v1/Chassis/{}", id)).await
}
pub async fn list_managers(&self) -> Result<Collection> {
self.get_as("/redfish/v1/Managers").await
}
pub async fn get_manager(&self, id: &str) -> Result<Manager> {
self.get_as(&format!("/redfish/v1/Managers/{}", id)).await
}
pub async fn get_power(&self, chassis_id: &str) -> Result<Power> {
self.get_as(&format!("/redfish/v1/Chassis/{}/Power", chassis_id)).await
}
pub async fn get_thermal(&self, chassis_id: &str) -> Result<Thermal> {
self.get_as(&format!("/redfish/v1/Chassis/{}/Thermal", chassis_id)).await
}
pub async fn list_processors(&self, system_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Systems/{}/Processors", system_id)).await
}
pub async fn get_processor(&self, system_id: &str, proc_id: &str) -> Result<Processor> {
self.get_as(&format!("/redfish/v1/Systems/{}/Processors/{}", system_id, proc_id)).await
}
pub async fn list_memory(&self, system_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Systems/{}/Memory", system_id)).await
}
pub async fn get_memory(&self, system_id: &str, mem_id: &str) -> Result<Memory> {
self.get_as(&format!("/redfish/v1/Systems/{}/Memory/{}", system_id, mem_id)).await
}
pub async fn list_storage(&self, system_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Systems/{}/Storage", system_id)).await
}
pub async fn get_storage(&self, system_id: &str, storage_id: &str) -> Result<Storage> {
self.get_as(&format!("/redfish/v1/Systems/{}/Storage/{}", system_id, storage_id)).await
}
pub async fn list_ethernet_interfaces(&self, system_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Systems/{}/EthernetInterfaces", system_id)).await
}
pub async fn get_ethernet_interface(&self, system_id: &str, iface_id: &str) -> Result<EthernetInterface> {
self.get_as(&format!("/redfish/v1/Systems/{}/EthernetInterfaces/{}", system_id, iface_id)).await
}
pub async fn get_account_service(&self) -> Result<AccountService> {
self.get_as("/redfish/v1/AccountService").await
}
pub async fn list_accounts(&self) -> Result<Collection> {
self.get_as("/redfish/v1/AccountService/Accounts").await
}
pub async fn get_update_service(&self) -> Result<UpdateService> {
self.get_as("/redfish/v1/UpdateService").await
}
pub async fn get_event_service(&self) -> Result<EventService> {
self.get_as("/redfish/v1/EventService").await
}
pub async fn list_log_entries(&self, manager_id: &str, log_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Managers/{}/LogServices/{}/Entries", manager_id, log_id)).await
}
pub async fn reset_system(&self, system_id: &str, reset_type: &str) -> Result<Value> {
let path = format!("/redfish/v1/Systems/{}/Actions/ComputerSystem.Reset", system_id);
let body = serde_json::json!({ "ResetType": reset_type });
self.post(&path, &body).await
}
pub async fn power_on(&self, system_id: &str) -> Result<Value> {
self.reset_system(system_id, "On").await
}
pub async fn power_off(&self, system_id: &str) -> Result<Value> {
self.reset_system(system_id, "ForceOff").await
}
pub async fn graceful_shutdown(&self, system_id: &str) -> Result<Value> {
self.reset_system(system_id, "GracefulShutdown").await
}
pub async fn graceful_restart(&self, system_id: &str) -> Result<Value> {
self.reset_system(system_id, "GracefulRestart").await
}
pub async fn force_restart(&self, system_id: &str) -> Result<Value> {
self.reset_system(system_id, "ForceRestart").await
}
pub async fn power_cycle(&self, system_id: &str) -> Result<Value> {
self.reset_system(system_id, "PowerCycle").await
}
pub async fn set_boot_override(&self, system_id: &str, target: &str, enabled: Option<&str>) -> Result<Value> {
let path = format!("/redfish/v1/Systems/{}", system_id);
let body = serde_json::json!({
"Boot": {
"BootSourceOverrideTarget": target,
"BootSourceOverrideEnabled": enabled.unwrap_or("Once")
}
});
self.patch(&path, &body).await
}
pub async fn set_boot_pxe(&self, system_id: &str) -> Result<Value> {
self.set_boot_override(system_id, "Pxe", Some("Once")).await
}
pub async fn set_boot_bios(&self, system_id: &str) -> Result<Value> {
self.set_boot_override(system_id, "BiosSetup", Some("Once")).await
}
pub async fn reset_manager(&self, manager_id: &str, reset_type: &str) -> Result<Value> {
let path = format!("/redfish/v1/Managers/{}/Actions/Manager.Reset", manager_id);
let body = serde_json::json!({ "ResetType": reset_type });
self.post(&path, &body).await
}
pub async fn clear_log(&self, manager_id: &str, log_id: &str) -> Result<Value> {
let path = format!("/redfish/v1/Managers/{}/LogServices/{}/Actions/LogService.ClearLog", manager_id, log_id);
self.post(&path, &serde_json::json!({})).await
}
pub async fn list_virtual_media(&self, manager_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Managers/{}/VirtualMedia", manager_id)).await
}
pub async fn get_virtual_media(&self, manager_id: &str, media_id: &str) -> Result<VirtualMedia> {
self.get_as(&format!("/redfish/v1/Managers/{}/VirtualMedia/{}", manager_id, media_id)).await
}
pub async fn insert_media(&self, manager_id: &str, media_id: &str, image_url: &str) -> Result<Value> {
let path = format!("/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.InsertMedia", manager_id, media_id);
self.post(&path, &serde_json::json!({ "Image": image_url })).await
}
pub async fn eject_media(&self, manager_id: &str, media_id: &str) -> Result<Value> {
let path = format!("/redfish/v1/Managers/{}/VirtualMedia/{}/Actions/VirtualMedia.EjectMedia", manager_id, media_id);
self.post(&path, &serde_json::json!({})).await
}
pub async fn get_bios(&self, system_id: &str) -> Result<Bios> {
self.get_as(&format!("/redfish/v1/Systems/{}/Bios", system_id)).await
}
pub async fn get_bios_settings(&self, system_id: &str) -> Result<Bios> {
self.get_as(&format!("/redfish/v1/Systems/{}/Bios/Settings", system_id)).await
}
pub async fn set_bios_attributes(&self, system_id: &str, attributes: &Value) -> Result<Value> {
let path = format!("/redfish/v1/Systems/{}/Bios/Settings", system_id);
self.patch(&path, &serde_json::json!({ "Attributes": attributes })).await
}
pub async fn reset_bios_defaults(&self, system_id: &str) -> Result<Value> {
let path = format!("/redfish/v1/Systems/{}/Bios/Actions/Bios.ResetBios", system_id);
self.post(&path, &serde_json::json!({})).await
}
pub async fn get_bios_attributes_full(&self, system_id: &str) -> Result<Vec<RegistryAttribute>> {
let bios = self.get(&format!("/redfish/v1/Systems/{}/Bios", system_id)).await?;
let pending = self.get(&format!("/redfish/v1/Systems/{}/Bios/Settings", system_id)).await.ok();
let current_attrs = bios.pointer("/Attributes");
let pending_attrs = pending.as_ref().and_then(|v| v.pointer("/Attributes"));
let registry_json = self.fetch_bios_registry(system_id).await?;
Ok(registry::parse_bios_registry(®istry_json, current_attrs, pending_attrs))
}
pub async fn list_registries(&self) -> Result<Collection> {
self.get_as("/redfish/v1/Registries").await
}
pub async fn get_registry(&self, registry_id: &str) -> Result<Value> {
let entry = self.get(&format!("/redfish/v1/Registries/{}", registry_id)).await?;
self.resolve_registry_location(&entry).await
}
pub async fn get_message_registry(&self, registry_id: &str) -> Result<(RegistryInfo, Vec<RegistryMessage>)> {
let json = self.get_registry(registry_id).await?;
Ok(registry::parse_message_registry(&json))
}
pub async fn get_attributes_full(
&self,
registry_id: &str,
resource_path: &str,
) -> Result<Vec<RegistryAttribute>> {
let registry_json = self.get_registry(registry_id).await?;
let resource = self.get(resource_path).await.ok();
let current_attrs = resource.as_ref().and_then(|v| v.pointer("/Attributes"));
Ok(registry::parse_attribute_registry(®istry_json, current_attrs))
}
async fn fetch_bios_registry(&self, system_id: &str) -> Result<Value> {
let bios = self.get(&format!("/redfish/v1/Systems/{}/Bios", system_id)).await?;
if let Some(reg_id) = bios.get("AttributeRegistry").and_then(|v| v.as_str()) {
return self.get_registry(reg_id).await;
}
let registries = self.get("/redfish/v1/Registries").await?;
if let Some(members) = registries.get("Members").and_then(|v| v.as_array()) {
for member in members {
if let Some(id) = member.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(entry) = self.get(id).await {
let reg_id = entry.get("Id").and_then(|v| v.as_str()).unwrap_or("");
if reg_id.contains("Bios") || reg_id.contains("BIOS") {
return self.resolve_registry_location(&entry).await;
}
}
}
}
}
Err(RedfishError::NotFound("BIOS attribute registry".to_string()))
}
async fn resolve_registry_location(&self, registry_entry: &Value) -> Result<Value> {
let locations = registry_entry
.get("Location")
.and_then(|v| v.as_array())
.ok_or_else(|| RedfishError::NotFound("Registry location".to_string()))?;
for loc in locations {
if let Some(uri) = loc.get("Uri").and_then(|v| v.as_str()) {
if let Ok(body) = self.get(uri).await {
return Ok(body);
}
}
if let Some(archive_uri) = loc.get("ArchiveUri").and_then(|v| v.as_str()) {
if let Ok(body) = self.get(archive_uri).await {
return Ok(body);
}
}
}
Err(RedfishError::NotFound("Registry content".to_string()))
}
pub async fn get_secure_boot(&self, system_id: &str) -> Result<SecureBoot> {
self.get_as(&format!("/redfish/v1/Systems/{}/SecureBoot", system_id)).await
}
pub async fn set_secure_boot(&self, system_id: &str, enabled: bool) -> Result<Value> {
let path = format!("/redfish/v1/Systems/{}/SecureBoot", system_id);
self.patch(&path, &serde_json::json!({ "SecureBootEnable": enabled })).await
}
pub async fn get_network_protocol(&self, manager_id: &str) -> Result<NetworkProtocol> {
self.get_as(&format!("/redfish/v1/Managers/{}/NetworkProtocol", manager_id)).await
}
pub async fn set_network_protocol(&self, manager_id: &str, settings: &Value) -> Result<Value> {
self.patch(&format!("/redfish/v1/Managers/{}/NetworkProtocol", manager_id), settings).await
}
pub async fn list_serial_interfaces(&self, manager_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Managers/{}/SerialInterfaces", manager_id)).await
}
pub async fn get_serial_interface(&self, manager_id: &str, iface_id: &str) -> Result<SerialInterface> {
self.get_as(&format!("/redfish/v1/Managers/{}/SerialInterfaces/{}", manager_id, iface_id)).await
}
pub async fn list_volumes(&self, system_id: &str, storage_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Systems/{}/Storage/{}/Volumes", system_id, storage_id)).await
}
pub async fn get_volume(&self, system_id: &str, storage_id: &str, volume_id: &str) -> Result<Volume> {
self.get_as(&format!("/redfish/v1/Systems/{}/Storage/{}/Volumes/{}", system_id, storage_id, volume_id)).await
}
pub async fn create_volume(&self, system_id: &str, storage_id: &str, body: &Value) -> Result<Value> {
let path = format!("/redfish/v1/Systems/{}/Storage/{}/Volumes", system_id, storage_id);
self.post(&path, body).await
}
pub async fn delete_volume(&self, system_id: &str, storage_id: &str, volume_id: &str) -> Result<()> {
self.delete(&format!("/redfish/v1/Systems/{}/Storage/{}/Volumes/{}", system_id, storage_id, volume_id)).await
}
pub async fn get_drive(&self, path: &str) -> Result<Drive> {
self.get_as(path).await
}
pub async fn list_certificates(&self, manager_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Managers/{}/NetworkProtocol/HTTPS/Certificates", manager_id)).await
}
pub async fn get_certificate(&self, path: &str) -> Result<Certificate> {
self.get_as(path).await
}
pub async fn replace_certificate(&self, path: &str, cert_pem: &str, cert_type: &str) -> Result<Value> {
self.post(path, &serde_json::json!({
"CertificateString": cert_pem,
"CertificateType": cert_type
})).await
}
pub async fn list_subscriptions(&self) -> Result<Collection> {
self.get_as("/redfish/v1/EventService/Subscriptions").await
}
pub async fn create_subscription(&self, destination: &str, event_types: &[&str], context: &str) -> Result<Value> {
let types: Vec<String> = event_types.iter().map(|s| s.to_string()).collect();
self.post("/redfish/v1/EventService/Subscriptions", &serde_json::json!({
"Destination": destination,
"EventTypes": types,
"Protocol": "Redfish",
"Context": context
})).await
}
pub async fn delete_subscription(&self, subscription_id: &str) -> Result<()> {
self.delete(&format!("/redfish/v1/EventService/Subscriptions/{}", subscription_id)).await
}
pub async fn list_firmware_inventory(&self) -> Result<Collection> {
self.get_as("/redfish/v1/UpdateService/FirmwareInventory").await
}
pub async fn get_firmware_item(&self, item_id: &str) -> Result<SoftwareInventory> {
self.get_as(&format!("/redfish/v1/UpdateService/FirmwareInventory/{}", item_id)).await
}
pub async fn simple_update(&self, image_uri: &str) -> Result<Value> {
self.post("/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate", &serde_json::json!({
"ImageURI": image_uri
})).await
}
pub async fn list_tasks(&self) -> Result<Collection> {
self.get_as("/redfish/v1/TaskService/Tasks").await
}
pub async fn get_task(&self, task_id: &str) -> Result<Task> {
self.get_as(&format!("/redfish/v1/TaskService/Tasks/{}", task_id)).await
}
pub async fn wait_task(&self, task_id: &str, max_wait_secs: u64) -> Result<Task> {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(max_wait_secs);
loop {
let task = self.get_task(task_id).await?;
match task.task_state.as_deref() {
Some("Completed") | Some("Exception") | Some("Killed") | Some("Cancelled") => {
return Ok(task);
}
_ => {}
}
if std::time::Instant::now() > deadline {
return Ok(task);
}
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
}
}
pub async fn get_all_members(&self, path: &str) -> Result<Vec<OdataLink>> {
let mut all = Vec::new();
let mut current_path = path.to_string();
loop {
let val = self.get(¤t_path).await?;
if let Some(members) = val.get("Members").and_then(|m| m.as_array()) {
for m in members {
if let Some(id) = m.get("@odata.id").and_then(|v| v.as_str()) {
all.push(OdataLink { odata_id: id.to_string() });
}
}
}
match val.get("Members@odata.nextLink").and_then(|v| v.as_str()) {
Some(next) => current_path = next.to_string(),
None => break,
}
}
Ok(all)
}
pub async fn list_manager_ethernet_interfaces(&self, manager_id: &str) -> Result<Collection> {
self.get_as(&format!("/redfish/v1/Managers/{}/EthernetInterfaces", manager_id)).await
}
pub async fn get_manager_ethernet_interface(&self, manager_id: &str, iface_id: &str) -> Result<EthernetInterface> {
self.get_as(&format!("/redfish/v1/Managers/{}/EthernetInterfaces/{}", manager_id, iface_id)).await
}
pub async fn patch_manager_ethernet_interface(&self, manager_id: &str, iface_id: &str, body: &Value) -> Result<Value> {
self.patch(&format!("/redfish/v1/Managers/{}/EthernetInterfaces/{}", manager_id, iface_id), body).await
}
pub async fn get_chassis_indicator(&self, chassis_id: &str) -> Result<Option<bool>> {
let val = self.get(&format!("/redfish/v1/Chassis/{}", chassis_id)).await?;
if let Some(active) = val.get("LocationIndicatorActive").and_then(|v| v.as_bool()) {
return Ok(Some(active));
}
if let Some(led) = val.get("IndicatorLED").and_then(|v| v.as_str()) {
return Ok(Some(led != "Off"));
}
Ok(None)
}
pub async fn set_chassis_indicator(&self, chassis_id: &str, on: bool) -> Result<Value> {
let path = format!("/redfish/v1/Chassis/{}", chassis_id);
let val = self.get(&path).await?;
if val.get("LocationIndicatorActive").is_some() {
self.patch(&path, &serde_json::json!({"LocationIndicatorActive": on})).await
} else {
let led = if on { "Lit" } else { "Off" };
self.patch(&path, &serde_json::json!({"IndicatorLED": led})).await
}
}
pub async fn get_system_health(&self, system_id: &str, chassis_id: &str) -> Result<SystemHealth> {
let system = self.get(&format!("/redfish/v1/Systems/{}", system_id)).await?;
let thermal = self.get(&format!("/redfish/v1/Chassis/{}/Thermal", chassis_id)).await.ok();
let power = self.get(&format!("/redfish/v1/Chassis/{}/Power", chassis_id)).await.ok();
Ok(enriched::parse_system_health(&system, thermal.as_ref(), power.as_ref()))
}
pub async fn get_enriched_logs(
&self,
manager_id: &str,
log_id: &str,
) -> Result<Vec<EnrichedLogEntry>> {
let entries_val = self.get(&format!(
"/redfish/v1/Managers/{}/LogServices/{}/Entries", manager_id, log_id
)).await?;
let mut msg_map = std::collections::HashMap::new();
if let Ok((_, messages)) = self.get_message_registry("Base").await {
for m in messages {
msg_map.insert(m.message_id.clone(), (m.message.clone(), m.resolution.clone()));
}
}
let entries = entries_val
.get("Members")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
Ok(entries
.iter()
.filter_map(|e| enriched::parse_enriched_log_entry(e, &msg_map))
.collect())
}
pub async fn get_storage_overview(&self, system_id: &str) -> Result<StorageOverview> {
let storage_collection = self.get(&format!("/redfish/v1/Systems/{}/Storage", system_id)).await?;
let members = storage_collection
.get("Members")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut controllers = Vec::new();
let mut volumes = Vec::new();
let mut all_drives = Vec::new();
let mut assigned_drive_ids = std::collections::HashSet::new();
for member in &members {
let path = match member.get("@odata.id").and_then(|v| v.as_str()) {
Some(p) => p,
None => continue,
};
let storage = match self.get(path).await {
Ok(s) => s,
Err(_) => continue,
};
if let Some(ctrls) = storage.get("StorageControllers").and_then(|v| v.as_array()) {
for c in ctrls {
if let Some(name) = c.get("Name").and_then(|v| v.as_str()) {
controllers.push(ControllerSummary {
name: name.to_string(),
manufacturer: c.get("Manufacturer").and_then(|v| v.as_str()).map(String::from),
model: c.get("Model").and_then(|v| v.as_str()).map(String::from),
firmware_version: c.get("FirmwareVersion").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
c.pointer("/Status/Health").and_then(|v| v.as_str()),
),
});
}
}
}
if let Some(drives) = storage.get("Drives").and_then(|v| v.as_array()) {
for d in drives {
if let Some(drive_path) = d.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(drive) = self.get(drive_path).await {
let id = drive.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string();
all_drives.push((drive_path.to_string(), DriveSummary {
id: id.clone(),
name: drive.get("Name").and_then(|v| v.as_str()).unwrap_or(&id).to_string(),
manufacturer: drive.get("Manufacturer").and_then(|v| v.as_str()).map(String::from),
model: drive.get("Model").and_then(|v| v.as_str()).map(String::from),
capacity_bytes: drive.get("CapacityBytes").and_then(|v| v.as_u64()),
media_type: drive.get("MediaType").and_then(|v| v.as_str()).map(String::from),
protocol: drive.get("Protocol").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
drive.pointer("/Status/Health").and_then(|v| v.as_str()),
),
}));
}
}
}
}
let volumes_path = format!("{}/Volumes", path);
if let Ok(vol_collection) = self.get(&volumes_path).await {
if let Some(vol_members) = vol_collection.get("Members").and_then(|v| v.as_array()) {
for vm in vol_members {
if let Some(vol_path) = vm.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(vol) = self.get(vol_path).await {
let drive_links = vol.get("Links")
.and_then(|l| l.get("Drives"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|d| d.get("@odata.id").and_then(|v| v.as_str()))
.map(String::from)
.collect::<Vec<_>>()
})
.unwrap_or_default();
for dl in &drive_links {
assigned_drive_ids.insert(dl.clone());
}
volumes.push(VolumeSummary {
id: vol.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: vol.get("Name").and_then(|v| v.as_str()).unwrap_or("").to_string(),
capacity_bytes: vol.get("CapacityBytes").and_then(|v| v.as_u64()),
raid_type: vol.get("RAIDType").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
vol.pointer("/Status/Health").and_then(|v| v.as_str()),
),
drive_count: drive_links.len(),
});
}
}
}
}
}
}
let unassigned_drives = all_drives
.into_iter()
.filter(|(path, _)| !assigned_drive_ids.contains(path.as_str()))
.map(|(_, d)| d)
.collect();
Ok(StorageOverview { controllers, volumes, unassigned_drives })
}
pub async fn list_users(&self) -> Result<Vec<UserAccount>> {
let collection = self.get("/redfish/v1/AccountService/Accounts").await?;
let members = collection
.get("Members")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let mut users = Vec::new();
for m in &members {
if let Some(path) = m.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(acct) = self.get(path).await {
let username = acct.get("UserName").and_then(|v| v.as_str()).unwrap_or("");
if username.is_empty() { continue; }
users.push(UserAccount {
id: acct.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
username: username.to_string(),
role: acct.get("RoleId").and_then(|v| v.as_str()).map(String::from),
enabled: acct.get("Enabled").and_then(|v| v.as_bool()).unwrap_or(true),
locked: acct.get("Locked").and_then(|v| v.as_bool()).unwrap_or(false),
});
}
}
}
Ok(users)
}
pub async fn create_user(
&self,
username: &str,
password: &str,
role: &str,
) -> Result<Value> {
self.post("/redfish/v1/AccountService/Accounts", &serde_json::json!({
"UserName": username,
"Password": password,
"RoleId": role,
"Enabled": true
})).await
}
pub async fn change_password(&self, account_id: &str, new_password: &str) -> Result<Value> {
self.patch(
&format!("/redfish/v1/AccountService/Accounts/{}", account_id),
&serde_json::json!({ "Password": new_password }),
).await
}
pub async fn set_user_role(&self, account_id: &str, role: &str) -> Result<Value> {
self.patch(
&format!("/redfish/v1/AccountService/Accounts/{}", account_id),
&serde_json::json!({ "RoleId": role }),
).await
}
pub async fn set_user_enabled(&self, account_id: &str, enabled: bool) -> Result<Value> {
self.patch(
&format!("/redfish/v1/AccountService/Accounts/{}", account_id),
&serde_json::json!({ "Enabled": enabled }),
).await
}
pub async fn unlock_user(&self, account_id: &str) -> Result<Value> {
self.patch(
&format!("/redfish/v1/AccountService/Accounts/{}", account_id),
&serde_json::json!({ "Locked": false }),
).await
}
pub async fn delete_user(&self, account_id: &str) -> Result<()> {
self.delete(&format!("/redfish/v1/AccountService/Accounts/{}", account_id)).await
}
pub async fn subscribe(&self, params: &SubscribeParams<'_>) -> Result<Value> {
let mut body = serde_json::json!({
"Destination": params.destination,
"Protocol": "Redfish",
"Context": params.context.unwrap_or("rufish-subscription"),
});
if let Some(types) = params.event_types {
body["EventTypes"] = serde_json::json!(types);
}
if let Some(severity) = params.severity_filter {
body["RegistryPrefixes"] = serde_json::json!(["Base"]);
body["MessageIds"] = Value::Null;
body["Context"] = serde_json::json!(
format!("{}|severity={}", params.context.unwrap_or("rufish"), severity)
);
}
self.post("/redfish/v1/EventService/Subscriptions", &body).await
}
pub async fn subscribe_sse(&self) -> Result<EventStream> {
let event_svc = self.get("/redfish/v1/EventService").await?;
let sse_uri = event_svc
.get("ServerSentEventUri")
.and_then(|v| v.as_str())
.unwrap_or("/redfish/v1/SSE");
let url = format!("{}{}", self.base_url, sse_uri);
let mut req = self.client.get(&url).header("Accept", "text/event-stream");
if let Some(token) = &self.session_token {
req = req.header("X-Auth-Token", token);
} else {
req = req.basic_auth(&self.username, Some(&self.password));
}
let resp = req.send().await?;
if !resp.status().is_success() {
return Err(RedfishError::Api {
status: resp.status().as_u16(),
message: "SSE connection failed".to_string(),
});
}
Ok(EventStream::new(resp))
}
pub async fn export_bios_config(&self, system_id: &str) -> Result<BiosConfig> {
let system = self.get(&format!("/redfish/v1/Systems/{}", system_id)).await?;
let bios = self.get(&format!("/redfish/v1/Systems/{}/Bios", system_id)).await?;
let attributes = bios
.pointer("/Attributes")
.and_then(|v| v.as_object())
.map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default();
Ok(BiosConfig {
source: Some(BiosConfigSource {
host: Some(self.base_url.clone()),
system_id: Some(system_id.to_string()),
model: system.get("Model").and_then(|v| v.as_str()).map(String::from),
bios_version: system.get("BiosVersion").and_then(|v| v.as_str()).map(String::from),
exported_at: Some(chrono_now()),
}),
attributes,
})
}
pub async fn import_bios_config(&self, system_id: &str, config: &BiosConfig) -> Result<Value> {
let attrs = serde_json::to_value(&config.attributes)
.map_err(|e| RedfishError::Parse(e.to_string()))?;
self.set_bios_attributes(system_id, &attrs).await
}
pub async fn diff_bios_config(
&self,
system_id: &str,
config: &BiosConfig,
) -> Result<crate::bios_config::BiosConfigDiff> {
let current = self.export_bios_config(system_id).await?;
Ok(config.diff(¤t))
}
pub async fn get_topology(&self, system_id: &str) -> Result<HardwareTopology> {
let sys_val = self.get(&format!("/redfish/v1/Systems/{}", system_id)).await?;
let mut processors = Vec::new();
if let Ok(proc_col) = self.get(&format!("/redfish/v1/Systems/{}/Processors", system_id)).await {
if let Some(members) = proc_col.get("Members").and_then(|v| v.as_array()) {
for m in members {
if let Some(path) = m.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(p) = self.get(path).await {
processors.push(ComponentNode {
id: p.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: p.get("Name").and_then(|v| v.as_str()).map(String::from),
description: p.get("Model").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
p.pointer("/Status/Health").and_then(|v| v.as_str()),
),
});
}
}
}
}
}
let mut memory = Vec::new();
if let Ok(mem_col) = self.get(&format!("/redfish/v1/Systems/{}/Memory", system_id)).await {
if let Some(members) = mem_col.get("Members").and_then(|v| v.as_array()) {
for m in members {
if let Some(path) = m.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(mem) = self.get(path).await {
let cap = mem.get("CapacityMiB").and_then(|v| v.as_u64());
let desc = cap.map(|c| format!("{} MiB", c));
memory.push(ComponentNode {
id: mem.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: mem.get("Name").and_then(|v| v.as_str()).map(String::from),
description: desc,
status: HealthStatus::from_str_opt(
mem.pointer("/Status/Health").and_then(|v| v.as_str()),
),
});
}
}
}
}
}
let system_node = SystemNode {
id: system_id.to_string(),
name: sys_val.get("Name").and_then(|v| v.as_str()).map(String::from),
model: sys_val.get("Model").and_then(|v| v.as_str()).map(String::from),
manufacturer: sys_val.get("Manufacturer").and_then(|v| v.as_str()).map(String::from),
serial_number: sys_val.get("SerialNumber").and_then(|v| v.as_str()).map(String::from),
power_state: sys_val.get("PowerState").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
sys_val.pointer("/Status/Health").and_then(|v| v.as_str()),
),
processors,
memory,
};
let mut chassis_nodes = Vec::new();
if let Ok(chassis_col) = self.get("/redfish/v1/Chassis").await {
if let Some(members) = chassis_col.get("Members").and_then(|v| v.as_array()) {
for m in members {
if let Some(path) = m.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(c) = self.get(path).await {
chassis_nodes.push(ChassisNode {
id: c.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: c.get("Name").and_then(|v| v.as_str()).map(String::from),
chassis_type: c.get("ChassisType").and_then(|v| v.as_str()).map(String::from),
model: c.get("Model").and_then(|v| v.as_str()).map(String::from),
serial_number: c.get("SerialNumber").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
c.pointer("/Status/Health").and_then(|v| v.as_str()),
),
});
}
}
}
}
}
let mut manager_nodes = Vec::new();
if let Ok(mgr_col) = self.get("/redfish/v1/Managers").await {
if let Some(members) = mgr_col.get("Members").and_then(|v| v.as_array()) {
for m in members {
if let Some(path) = m.get("@odata.id").and_then(|v| v.as_str()) {
if let Ok(mgr) = self.get(path).await {
manager_nodes.push(ManagerNode {
id: mgr.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string(),
name: mgr.get("Name").and_then(|v| v.as_str()).map(String::from),
manager_type: mgr.get("ManagerType").and_then(|v| v.as_str()).map(String::from),
firmware_version: mgr.get("FirmwareVersion").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
mgr.pointer("/Status/Health").and_then(|v| v.as_str()),
),
});
}
}
}
}
}
Ok(HardwareTopology {
system: system_node,
chassis: chassis_nodes,
managers: manager_nodes,
})
}
pub async fn update_firmware_with_progress<F>(
&self,
image_uri: &str,
max_wait_secs: u64,
mut progress_cb: F,
) -> Result<Task>
where
F: FnMut(&str, Option<u32>),
{
let resp = self.post(
"/redfish/v1/UpdateService/Actions/UpdateService.SimpleUpdate",
&serde_json::json!({ "ImageURI": image_uri }),
).await?;
let task_id = resp
.pointer("/Id")
.or_else(|| resp.pointer("/@odata.id"))
.and_then(|v| v.as_str())
.map(|s| s.rsplit('/').next().unwrap_or(s).to_string())
.ok_or_else(|| RedfishError::Parse("No task ID in update response".to_string()))?;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(max_wait_secs);
loop {
let task_val = self.get(&format!("/redfish/v1/TaskService/Tasks/{}", task_id)).await?;
let state = task_val.get("TaskState").and_then(|v| v.as_str()).unwrap_or("Unknown");
let percent = task_val.get("PercentComplete").and_then(|v| v.as_u64()).map(|p| p as u32);
progress_cb(state, percent);
match state {
"Completed" | "Exception" | "Killed" | "Cancelled" => {
return self.get_task(&task_id).await;
}
_ => {}
}
if std::time::Instant::now() > deadline {
return self.get_task(&task_id).await;
}
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
}
}
pub async fn get_with_retry(&mut self, path: &str, max_retries: u32) -> Result<Value> {
let mut retries = 0;
loop {
match self.get(path).await {
Ok(val) => return Ok(val),
Err(RedfishError::SessionExpired) if retries < max_retries => {
log::debug!("Session expired, re-authenticating (attempt {})", retries + 1);
self.login().await?;
retries += 1;
}
Err(RedfishError::Http(_)) if retries < max_retries => {
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1 << retries)).await;
}
Err(e) => return Err(e),
}
}
}
pub async fn patch_with_retry(
&mut self,
path: &str,
body: &Value,
max_retries: u32,
) -> Result<Value> {
let mut retries = 0;
loop {
match self.patch(path, body).await {
Ok(val) => return Ok(val),
Err(RedfishError::SessionExpired) if retries < max_retries => {
self.login().await?;
retries += 1;
}
Err(RedfishError::Http(_)) if retries < max_retries => {
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1 << retries)).await;
}
Err(e) => return Err(e),
}
}
}
pub async fn post_with_retry(
&mut self,
path: &str,
body: &Value,
max_retries: u32,
) -> Result<Value> {
let mut retries = 0;
loop {
match self.post(path, body).await {
Ok(val) => return Ok(val),
Err(RedfishError::SessionExpired) if retries < max_retries => {
self.login().await?;
retries += 1;
}
Err(RedfishError::Http(_)) if retries < max_retries => {
retries += 1;
tokio::time::sleep(std::time::Duration::from_secs(1 << retries)).await;
}
Err(e) => return Err(e),
}
}
}
async fn auth_get(&self, url: &str) -> Result<Response> {
let mut req = self.client.get(url);
if let Some(token) = &self.session_token {
req = req.header("X-Auth-Token", token);
} else {
req = req.basic_auth(&self.username, Some(&self.password));
}
Ok(req.send().await?)
}
async fn handle_response(&self, resp: Response) -> Result<Value> {
let status = resp.status();
if status.is_success() {
let body = resp.text().await?;
if body.is_empty() {
Ok(Value::Null)
} else {
serde_json::from_str(&body).map_err(|e| RedfishError::Parse(e.to_string()))
}
} else if status == StatusCode::NOT_FOUND {
Err(RedfishError::NotFound(resp.url().to_string()))
} else if status == StatusCode::UNAUTHORIZED {
Err(RedfishError::SessionExpired)
} else {
let code = status.as_u16();
let text = resp.text().await.unwrap_or_default();
Err(RedfishError::Api { status: code, message: text })
}
}
}
fn chrono_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days = secs / 86400;
let time_secs = secs % 86400;
let hours = time_secs / 3600;
let minutes = (time_secs % 3600) / 60;
let seconds = time_secs % 60;
let mut y = 1970i64;
let mut remaining_days = days as i64;
loop {
let days_in_year = if y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) { 366 } else { 365 };
if remaining_days < days_in_year { break; }
remaining_days -= days_in_year;
y += 1;
}
let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
let month_days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
let mut m = 0;
for md in &month_days {
if remaining_days < *md { break; }
remaining_days -= md;
m += 1;
}
format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m + 1, remaining_days + 1, hours, minutes, seconds)
}