use log::debug;
use reqwest::{Client, Response, StatusCode};
use serde_json::Value;
use crate::error::{RedfishError, Result};
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 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
}
}
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 })
}
}
}