1pub mod cache;
17pub mod credentials;
18
19#[cfg(feature = "reqwest")]
20pub mod reqwest;
21
22use crate::cache::TypeErasedCarCache;
23use http::HeaderMap;
24use nv_redfish_core::query::ExpandQuery;
25use nv_redfish_core::Action;
26use nv_redfish_core::Bmc;
27use nv_redfish_core::BoxTryStream;
28use nv_redfish_core::EntityTypeRef;
29use nv_redfish_core::Expandable;
30use nv_redfish_core::FilterQuery;
31use nv_redfish_core::ModificationResponse;
32use nv_redfish_core::ODataETag;
33use nv_redfish_core::ODataId;
34use serde::{de::DeserializeOwned, Deserialize, Serialize};
35use std::{
36 collections::HashMap,
37 error::Error as StdError,
38 future::Future,
39 sync::{Arc, RwLock},
40};
41use url::Url;
42
43#[doc(inline)]
44pub use credentials::BmcCredentials;
45
46pub trait HttpClient: Send + Sync {
47 type Error: Send + StdError;
48
49 fn get<T>(
51 &self,
52 url: Url,
53 credentials: &BmcCredentials,
54 etag: Option<ODataETag>,
55 custom_headers: &HeaderMap,
56 ) -> impl Future<Output = Result<T, Self::Error>> + Send
57 where
58 T: DeserializeOwned + Send + Sync;
59
60 fn post<B, T>(
62 &self,
63 url: Url,
64 body: &B,
65 credentials: &BmcCredentials,
66 custom_headers: &HeaderMap,
67 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
68 where
69 B: Serialize + Send + Sync,
70 T: DeserializeOwned + Send + Sync;
71
72 fn patch<B, T>(
74 &self,
75 url: Url,
76 etag: ODataETag,
77 body: &B,
78 credentials: &BmcCredentials,
79 custom_headers: &HeaderMap,
80 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
81 where
82 B: Serialize + Send + Sync,
83 T: DeserializeOwned + Send + Sync;
84
85 fn delete<T>(
87 &self,
88 url: Url,
89 credentials: &BmcCredentials,
90 custom_headers: &HeaderMap,
91 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
92 where
93 T: DeserializeOwned + Send + Sync;
94
95 fn sse<T: Sized + for<'a> Deserialize<'a> + Send + 'static>(
97 &self,
98 url: Url,
99 credentials: &BmcCredentials,
100 custom_headers: &HeaderMap,
101 ) -> impl Future<Output = Result<BoxTryStream<T, Self::Error>, Self::Error>> + Send;
102}
103
104pub struct HttpBmc<C: HttpClient> {
115 client: C,
116 redfish_endpoint: RedfishEndpoint,
117 credentials: RwLock<Arc<BmcCredentials>>,
118 cache: RwLock<TypeErasedCarCache<ODataId>>,
119 etags: RwLock<HashMap<ODataId, ODataETag>>,
120 custom_headers: HeaderMap,
121}
122
123impl<C: HttpClient> HttpBmc<C>
124where
125 C::Error: CacheableError,
126{
127 pub fn new(
154 client: C,
155 redfish_endpoint: Url,
156 credentials: BmcCredentials,
157 cache_settings: CacheSettings,
158 ) -> Self {
159 Self::with_custom_headers(
160 client,
161 redfish_endpoint,
162 credentials,
163 cache_settings,
164 HeaderMap::new(),
165 )
166 }
167
168 pub fn with_custom_headers(
220 client: C,
221 redfish_endpoint: Url,
222 credentials: BmcCredentials,
223 cache_settings: CacheSettings,
224 custom_headers: HeaderMap,
225 ) -> Self {
226 Self {
227 client,
228 redfish_endpoint: RedfishEndpoint::from(redfish_endpoint),
229 credentials: RwLock::new(Arc::new(credentials)),
230 cache: RwLock::new(TypeErasedCarCache::new(cache_settings.capacity)),
231 etags: RwLock::new(HashMap::new()),
232 custom_headers,
233 }
234 }
235
236 pub fn set_credentials(&self, credentials: BmcCredentials) -> Result<(), String> {
244 let mut current = self.credentials.write().expect("poisoned");
245 *current = Arc::new(credentials);
246 Ok(())
247 }
248}
249
250#[derive(Debug, Clone)]
254pub struct RedfishEndpoint {
255 base_url: Url,
256}
257
258impl RedfishEndpoint {
259 #[must_use]
261 pub const fn new(base_url: Url) -> Self {
262 Self { base_url }
263 }
264
265 #[must_use]
267 pub fn with_path(&self, path: &str) -> Url {
268 let mut url = self.base_url.clone();
269 url.set_path(path);
270 url
271 }
272
273 #[must_use]
275 pub fn with_path_and_query(&self, path: &str, query: &str) -> Url {
276 let mut url = self.with_path(path);
277 url.set_query(Some(query));
278 url
279 }
280}
281
282pub struct CacheSettings {
284 capacity: usize,
285}
286
287impl Default for CacheSettings {
288 fn default() -> Self {
289 Self { capacity: 100 }
290 }
291}
292
293impl CacheSettings {
294 pub fn with_capacity(capacity: usize) -> Self {
295 Self { capacity }
296 }
297}
298
299impl From<Url> for RedfishEndpoint {
300 fn from(url: Url) -> Self {
301 Self::new(url)
302 }
303}
304
305impl From<&RedfishEndpoint> for Url {
306 fn from(endpoint: &RedfishEndpoint) -> Self {
307 endpoint.base_url.clone()
308 }
309}
310
311pub trait CacheableError {
314 fn is_cached(&self) -> bool;
317
318 fn cache_miss() -> Self;
320
321 fn cache_error(reason: String) -> Self;
323}
324
325impl<C: HttpClient> HttpBmc<C>
326where
327 C::Error: CacheableError + StdError + Send + Sync,
328{
329 fn read_credentials(&self) -> Arc<BmcCredentials> {
330 self.credentials
331 .read()
332 .map(|credentials| Arc::clone(&credentials))
333 .expect("lock poisoned")
334 }
335
336 #[allow(clippy::significant_drop_tightening)]
344 async fn get_with_cache<
345 T: EntityTypeRef + Sized + for<'de> Deserialize<'de> + 'static + Send + Sync,
346 >(
347 &self,
348 endpoint_url: Url,
349 id: &ODataId,
350 ) -> Result<Arc<T>, C::Error> {
351 let etag: Option<ODataETag> = {
353 let etags = self
354 .etags
355 .read()
356 .map_err(|e| C::Error::cache_error(e.to_string()))?;
357 etags.get(id).cloned()
358 };
359 let credentials = self.read_credentials();
360
361 match self
363 .client
364 .get::<T>(
365 endpoint_url,
366 credentials.as_ref(),
367 etag,
368 &self.custom_headers,
369 )
370 .await
371 {
372 Ok(response) => {
373 let entity = Arc::new(response);
374
375 if let Some(etag) = entity.etag() {
377 let mut cache = self
378 .cache
379 .write()
380 .map_err(|e| C::Error::cache_error(e.to_string()))?;
381
382 let mut etags = self
383 .etags
384 .write()
385 .map_err(|e| C::Error::cache_error(e.to_string()))?;
386
387 if let Some(evicted_id) = cache.put_typed(id.clone(), Arc::clone(&entity)) {
388 etags.remove(&evicted_id);
389 }
390 etags.insert(id.clone(), etag.clone());
391 }
392 Ok(entity)
393 }
394 Err(e) => {
395 if e.is_cached() {
397 let mut cache = self
398 .cache
399 .write()
400 .map_err(|e| C::Error::cache_error(e.to_string()))?;
401 cache
402 .get_typed::<Arc<T>>(id)
403 .cloned()
404 .ok_or_else(C::Error::cache_miss)
405 } else {
406 Err(e)
407 }
408 }
409 }
410 }
411}
412
413impl<C: HttpClient> Bmc for HttpBmc<C>
414where
415 C::Error: CacheableError + StdError + Send + Sync,
416{
417 type Error = C::Error;
418
419 async fn get<T: EntityTypeRef + Sized + for<'de> Deserialize<'de> + 'static + Send + Sync>(
420 &self,
421 id: &ODataId,
422 ) -> Result<Arc<T>, Self::Error> {
423 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
424 self.get_with_cache(endpoint_url, id).await
425 }
426
427 async fn expand<T: Expandable + Send + Sync + 'static>(
428 &self,
429 id: &ODataId,
430 query: ExpandQuery,
431 ) -> Result<Arc<T>, Self::Error> {
432 let endpoint_url = self
433 .redfish_endpoint
434 .with_path_and_query(&id.to_string(), &query.to_query_string());
435
436 self.get_with_cache(endpoint_url, id).await
437 }
438
439 async fn create<V: Sync + Send + Serialize, R: Sync + Send + for<'de> Deserialize<'de>>(
440 &self,
441 id: &ODataId,
442 v: &V,
443 ) -> Result<ModificationResponse<R>, Self::Error> {
444 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
445 let credentials = self.read_credentials();
446 self.client
447 .post(endpoint_url, v, credentials.as_ref(), &self.custom_headers)
448 .await
449 }
450
451 async fn update<V: Sync + Send + Serialize, R: Sync + Send + for<'de> Deserialize<'de>>(
452 &self,
453 id: &ODataId,
454 etag: Option<&ODataETag>,
455 v: &V,
456 ) -> Result<ModificationResponse<R>, Self::Error> {
457 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
458 let etag = etag
459 .cloned()
460 .unwrap_or_else(|| ODataETag::from(String::from("*")));
461 let credentials = self.read_credentials();
462 self.client
463 .patch(
464 endpoint_url,
465 etag,
466 v,
467 credentials.as_ref(),
468 &self.custom_headers,
469 )
470 .await
471 }
472
473 async fn delete<T: Sync + Send + for<'de> Deserialize<'de>>(
474 &self,
475 id: &ODataId,
476 ) -> Result<ModificationResponse<T>, Self::Error> {
477 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
478 let credentials = self.read_credentials();
479 self.client
480 .delete(endpoint_url, credentials.as_ref(), &self.custom_headers)
481 .await
482 }
483
484 async fn action<
485 T: Sync + Send + Serialize,
486 R: Sync + Send + Sized + for<'de> Deserialize<'de>,
487 >(
488 &self,
489 action: &Action<T, R>,
490 params: &T,
491 ) -> Result<ModificationResponse<R>, Self::Error> {
492 let endpoint_url = self.redfish_endpoint.with_path(&action.target.to_string());
493 let credentials = self.read_credentials();
494 self.client
495 .post(
496 endpoint_url,
497 params,
498 credentials.as_ref(),
499 &self.custom_headers,
500 )
501 .await
502 }
503
504 async fn filter<T: EntityTypeRef + Sized + for<'a> Deserialize<'a> + 'static + Send + Sync>(
505 &self,
506 id: &ODataId,
507 query: FilterQuery,
508 ) -> Result<Arc<T>, Self::Error> {
509 let endpoint_url = self
510 .redfish_endpoint
511 .with_path_and_query(&id.to_string(), &query.to_query_string());
512
513 self.get_with_cache(endpoint_url, id).await
514 }
515
516 async fn stream<T: Sized + for<'a> Deserialize<'a> + Send + 'static>(
517 &self,
518 uri: &str,
519 ) -> Result<BoxTryStream<T, Self::Error>, Self::Error> {
520 let endpoint_url = Url::parse(uri).unwrap_or_else(|_| self.redfish_endpoint.with_path(uri));
521 let credentials = self.read_credentials();
522 self.client
523 .sse(endpoint_url, credentials.as_ref(), &self.custom_headers)
524 .await
525 }
526}