use log::debug;
use reqwest::{Client, Response, StatusCode};
use serde_json::Value;
use crate::error::{RedfishError, Result};
use crate::types::*;
pub struct RedfishClient {
base_url: String,
username: String,
password: String,
client: Client,
session_token: Option<String>,
session_uri: Option<String>,
}
impl RedfishClient {
pub fn new(host: &str, username: &str, password: &str) -> Result<Self> {
let base_url = if host.starts_with("http") {
host.trim_end_matches('/').to_string()
} else {
format!("https://{}", host.trim_end_matches('/'))
};
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(30))
.build()?;
Ok(Self {
base_url,
username: username.to_string(),
password: password.to_string(),
client,
session_token: None,
session_uri: None,
})
}
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
}
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 })
}
}
}