pub mod cache;
pub mod credentials;
#[cfg(feature = "reqwest")]
pub mod reqwest;
use crate::cache::TypeErasedCarCache;
use http::HeaderMap;
use nv_redfish_core::query::ExpandQuery;
use nv_redfish_core::Action;
use nv_redfish_core::Bmc;
use nv_redfish_core::BoxTryStream;
use nv_redfish_core::EntityTypeRef;
use nv_redfish_core::Expandable;
use nv_redfish_core::FilterQuery;
use nv_redfish_core::ModificationResponse;
use nv_redfish_core::ODataETag;
use nv_redfish_core::ODataId;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use std::{
collections::HashMap,
error::Error as StdError,
future::Future,
sync::{Arc, RwLock},
};
use url::Url;
#[doc(inline)]
pub use credentials::BmcCredentials;
pub trait HttpClient: Send + Sync {
type Error: Send + StdError;
fn get<T>(
&self,
url: Url,
credentials: &BmcCredentials,
etag: Option<ODataETag>,
custom_headers: &HeaderMap,
) -> impl Future<Output = Result<T, Self::Error>> + Send
where
T: DeserializeOwned + Send + Sync;
fn post<B, T>(
&self,
url: Url,
body: &B,
credentials: &BmcCredentials,
custom_headers: &HeaderMap,
) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
where
B: Serialize + Send + Sync,
T: DeserializeOwned + Send + Sync;
fn patch<B, T>(
&self,
url: Url,
etag: ODataETag,
body: &B,
credentials: &BmcCredentials,
custom_headers: &HeaderMap,
) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
where
B: Serialize + Send + Sync,
T: DeserializeOwned + Send + Sync;
fn delete<T>(
&self,
url: Url,
credentials: &BmcCredentials,
custom_headers: &HeaderMap,
) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
where
T: DeserializeOwned + Send + Sync;
fn sse<T: Sized + for<'a> Deserialize<'a> + Send + 'static>(
&self,
url: Url,
credentials: &BmcCredentials,
custom_headers: &HeaderMap,
) -> impl Future<Output = Result<BoxTryStream<T, Self::Error>, Self::Error>> + Send;
}
pub struct HttpBmc<C: HttpClient> {
client: C,
redfish_endpoint: RedfishEndpoint,
credentials: RwLock<Arc<BmcCredentials>>,
cache: RwLock<TypeErasedCarCache<ODataId>>,
etags: RwLock<HashMap<ODataId, ODataETag>>,
custom_headers: HeaderMap,
}
impl<C: HttpClient> HttpBmc<C>
where
C::Error: CacheableError,
{
pub fn new(
client: C,
redfish_endpoint: Url,
credentials: BmcCredentials,
cache_settings: CacheSettings,
) -> Self {
Self::with_custom_headers(
client,
redfish_endpoint,
credentials,
cache_settings,
HeaderMap::new(),
)
}
pub fn with_custom_headers(
client: C,
redfish_endpoint: Url,
credentials: BmcCredentials,
cache_settings: CacheSettings,
custom_headers: HeaderMap,
) -> Self {
Self {
client,
redfish_endpoint: RedfishEndpoint::from(redfish_endpoint),
credentials: RwLock::new(Arc::new(credentials)),
cache: RwLock::new(TypeErasedCarCache::new(cache_settings.capacity)),
etags: RwLock::new(HashMap::new()),
custom_headers,
}
}
pub fn set_credentials(&self, credentials: BmcCredentials) -> Result<(), String> {
let mut current = self.credentials.write().expect("poisoned");
*current = Arc::new(credentials);
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct RedfishEndpoint {
base_url: Url,
}
impl RedfishEndpoint {
#[must_use]
pub const fn new(base_url: Url) -> Self {
Self { base_url }
}
#[must_use]
pub fn with_path(&self, path: &str) -> Url {
let mut url = self.base_url.clone();
url.set_path(path);
url
}
#[must_use]
pub fn with_path_and_query(&self, path: &str, query: &str) -> Url {
let mut url = self.with_path(path);
url.set_query(Some(query));
url
}
}
pub struct CacheSettings {
capacity: usize,
}
impl Default for CacheSettings {
fn default() -> Self {
Self { capacity: 100 }
}
}
impl CacheSettings {
pub fn with_capacity(capacity: usize) -> Self {
Self { capacity }
}
}
impl From<Url> for RedfishEndpoint {
fn from(url: Url) -> Self {
Self::new(url)
}
}
impl From<&RedfishEndpoint> for Url {
fn from(endpoint: &RedfishEndpoint) -> Self {
endpoint.base_url.clone()
}
}
pub trait CacheableError {
fn is_cached(&self) -> bool;
fn cache_miss() -> Self;
fn cache_error(reason: String) -> Self;
}
impl<C: HttpClient> HttpBmc<C>
where
C::Error: CacheableError + StdError + Send + Sync,
{
fn read_credentials(&self) -> Arc<BmcCredentials> {
self.credentials
.read()
.map(|credentials| Arc::clone(&credentials))
.expect("lock poisoned")
}
#[allow(clippy::significant_drop_tightening)]
async fn get_with_cache<
T: EntityTypeRef + Sized + for<'de> Deserialize<'de> + 'static + Send + Sync,
>(
&self,
endpoint_url: Url,
id: &ODataId,
) -> Result<Arc<T>, C::Error> {
let etag: Option<ODataETag> = {
let etags = self
.etags
.read()
.map_err(|e| C::Error::cache_error(e.to_string()))?;
etags.get(id).cloned()
};
let credentials = self.read_credentials();
match self
.client
.get::<T>(
endpoint_url,
credentials.as_ref(),
etag,
&self.custom_headers,
)
.await
{
Ok(response) => {
let entity = Arc::new(response);
if let Some(etag) = entity.etag() {
let mut cache = self
.cache
.write()
.map_err(|e| C::Error::cache_error(e.to_string()))?;
let mut etags = self
.etags
.write()
.map_err(|e| C::Error::cache_error(e.to_string()))?;
if let Some(evicted_id) = cache.put_typed(id.clone(), Arc::clone(&entity)) {
etags.remove(&evicted_id);
}
etags.insert(id.clone(), etag.clone());
}
Ok(entity)
}
Err(e) => {
if e.is_cached() {
let mut cache = self
.cache
.write()
.map_err(|e| C::Error::cache_error(e.to_string()))?;
cache
.get_typed::<Arc<T>>(id)
.cloned()
.ok_or_else(C::Error::cache_miss)
} else {
Err(e)
}
}
}
}
}
impl<C: HttpClient> Bmc for HttpBmc<C>
where
C::Error: CacheableError + StdError + Send + Sync,
{
type Error = C::Error;
async fn get<T: EntityTypeRef + Sized + for<'de> Deserialize<'de> + 'static + Send + Sync>(
&self,
id: &ODataId,
) -> Result<Arc<T>, Self::Error> {
let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
self.get_with_cache(endpoint_url, id).await
}
async fn expand<T: Expandable + Send + Sync + 'static>(
&self,
id: &ODataId,
query: ExpandQuery,
) -> Result<Arc<T>, Self::Error> {
let endpoint_url = self
.redfish_endpoint
.with_path_and_query(&id.to_string(), &query.to_query_string());
self.get_with_cache(endpoint_url, id).await
}
async fn create<V: Sync + Send + Serialize, R: Sync + Send + for<'de> Deserialize<'de>>(
&self,
id: &ODataId,
v: &V,
) -> Result<ModificationResponse<R>, Self::Error> {
let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
let credentials = self.read_credentials();
self.client
.post(endpoint_url, v, credentials.as_ref(), &self.custom_headers)
.await
}
async fn update<V: Sync + Send + Serialize, R: Sync + Send + for<'de> Deserialize<'de>>(
&self,
id: &ODataId,
etag: Option<&ODataETag>,
v: &V,
) -> Result<ModificationResponse<R>, Self::Error> {
let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
let etag = etag
.cloned()
.unwrap_or_else(|| ODataETag::from(String::from("*")));
let credentials = self.read_credentials();
self.client
.patch(
endpoint_url,
etag,
v,
credentials.as_ref(),
&self.custom_headers,
)
.await
}
async fn delete<T: Sync + Send + for<'de> Deserialize<'de>>(
&self,
id: &ODataId,
) -> Result<ModificationResponse<T>, Self::Error> {
let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
let credentials = self.read_credentials();
self.client
.delete(endpoint_url, credentials.as_ref(), &self.custom_headers)
.await
}
async fn action<
T: Sync + Send + Serialize,
R: Sync + Send + Sized + for<'de> Deserialize<'de>,
>(
&self,
action: &Action<T, R>,
params: &T,
) -> Result<ModificationResponse<R>, Self::Error> {
let endpoint_url = self.redfish_endpoint.with_path(&action.target.to_string());
let credentials = self.read_credentials();
self.client
.post(
endpoint_url,
params,
credentials.as_ref(),
&self.custom_headers,
)
.await
}
async fn filter<T: EntityTypeRef + Sized + for<'a> Deserialize<'a> + 'static + Send + Sync>(
&self,
id: &ODataId,
query: FilterQuery,
) -> Result<Arc<T>, Self::Error> {
let endpoint_url = self
.redfish_endpoint
.with_path_and_query(&id.to_string(), &query.to_query_string());
self.get_with_cache(endpoint_url, id).await
}
async fn stream<T: Sized + for<'a> Deserialize<'a> + Send + 'static>(
&self,
uri: &str,
) -> Result<BoxTryStream<T, Self::Error>, Self::Error> {
let endpoint_url = Url::parse(uri).unwrap_or_else(|_| self.redfish_endpoint.with_path(uri));
let credentials = self.read_credentials();
self.client
.sse(endpoint_url, credentials.as_ref(), &self.custom_headers)
.await
}
}