1#![deny(
17 clippy::all,
18 clippy::pedantic,
19 clippy::nursery,
20 clippy::suspicious,
21 clippy::complexity,
22 clippy::perf
23)]
24#![deny(
25 clippy::absolute_paths,
26 clippy::todo,
27 clippy::unimplemented,
28 clippy::tests_outside_test_module,
29 clippy::panic,
30 clippy::unwrap_used,
31 clippy::unwrap_in_result,
32 clippy::unused_trait_names,
33 clippy::print_stdout,
34 clippy::print_stderr
35)]
36#![allow(clippy::doc_markdown)]
37#![allow(clippy::duration_suboptimal_units)]
38#![deny(missing_docs)]
39
40pub mod cache;
43pub mod credentials;
44
45#[cfg(feature = "reqwest")]
46mod schema;
47
48#[cfg(feature = "reqwest")]
49pub mod reqwest;
50
51use std::collections::HashMap;
52use std::error::Error as StdError;
53use std::future::Future;
54use std::sync::Arc;
55use std::sync::RwLock;
56
57use crate::cache::TypeErasedCarCache;
58
59use http::HeaderMap;
60use nv_redfish_core::query::ExpandQuery;
61use nv_redfish_core::Action;
62use nv_redfish_core::Bmc;
63use nv_redfish_core::BoxTryStream;
64use nv_redfish_core::EntityTypeRef;
65use nv_redfish_core::Expandable;
66use nv_redfish_core::FilterQuery;
67use nv_redfish_core::ModificationResponse;
68use nv_redfish_core::ODataETag;
69use nv_redfish_core::ODataId;
70use nv_redfish_core::SessionCreateResponse;
71use nv_redfish_core::UploadReader;
72use serde::{de::DeserializeOwned, Deserialize, Serialize};
73use url::Url;
74
75#[doc(inline)]
76pub use credentials::BmcCredentials;
77
78#[cfg(feature = "update-service-deprecated")]
79#[doc(inline)]
80pub use nv_redfish_core::HttpPushUriUpdateRequest;
81#[cfg(feature = "update-service-deprecated")]
82#[doc(inline)]
83pub use nv_redfish_core::UploadStream;
84
85#[doc(inline)]
86pub use nv_redfish_core::MultipartUpdateRequest;
87
88pub trait HttpClient: Send + Sync {
93 type Error: Send + StdError;
95
96 fn get<T>(
98 &self,
99 url: Url,
100 credentials: &BmcCredentials,
101 etag: Option<ODataETag>,
102 custom_headers: &HeaderMap,
103 ) -> impl Future<Output = Result<T, Self::Error>> + Send
104 where
105 T: DeserializeOwned + Send + Sync;
106
107 fn post<B, T>(
109 &self,
110 url: Url,
111 body: &B,
112 credentials: &BmcCredentials,
113 custom_headers: &HeaderMap,
114 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
115 where
116 B: Serialize + Send + Sync,
117 T: DeserializeOwned + Send + Sync;
118
119 fn post_session<B, T>(
121 &self,
122 url: Url,
123 body: &B,
124 custom_headers: &HeaderMap,
125 ) -> impl Future<Output = Result<SessionCreateResponse<T>, Self::Error>> + Send
126 where
127 B: Serialize + Send + Sync,
128 T: DeserializeOwned + Send + Sync;
129
130 fn post_multipart_update<U, V, T>(
135 &self,
136 url: Url,
137 request: MultipartUpdateRequest<'_, U, V>,
138 credentials: &BmcCredentials,
139 custom_headers: &HeaderMap,
140 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
141 where
142 U: UploadReader,
143 T: DeserializeOwned + Send + Sync,
144 V: Serialize + Send + Sync;
145
146 #[cfg(feature = "update-service-deprecated")]
153 fn post_http_push_uri_update<U, T>(
154 &self,
155 url: Url,
156 request: HttpPushUriUpdateRequest<U>,
157 credentials: &BmcCredentials,
158 custom_headers: &HeaderMap,
159 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
160 where
161 U: UploadReader,
162 T: DeserializeOwned + Send + Sync;
163
164 fn patch<B, T>(
166 &self,
167 url: Url,
168 etag: ODataETag,
169 body: &B,
170 credentials: &BmcCredentials,
171 custom_headers: &HeaderMap,
172 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
173 where
174 B: Serialize + Send + Sync,
175 T: DeserializeOwned + Send + Sync;
176
177 fn delete<T>(
179 &self,
180 url: Url,
181 credentials: &BmcCredentials,
182 custom_headers: &HeaderMap,
183 ) -> impl Future<Output = Result<ModificationResponse<T>, Self::Error>> + Send
184 where
185 T: DeserializeOwned + Send + Sync;
186
187 fn sse<T: Sized + for<'de> Deserialize<'de> + Send>(
189 &self,
190 url: Url,
191 credentials: &BmcCredentials,
192 custom_headers: &HeaderMap,
193 ) -> impl Future<Output = Result<BoxTryStream<T, Self::Error>, Self::Error>> + Send;
194}
195
196pub struct HttpBmc<C: HttpClient> {
206 client: C,
207 redfish_endpoint: RedfishEndpoint,
208 credentials: RwLock<Arc<BmcCredentials>>,
209 cache: RwLock<TypeErasedCarCache<ODataId>>,
210 etags: RwLock<HashMap<ODataId, ODataETag>>,
211 custom_headers: HeaderMap,
212}
213
214impl<C: HttpClient> HttpBmc<C>
215where
216 C::Error: CacheableError,
217{
218 pub fn new(
245 client: C,
246 redfish_endpoint: Url,
247 credentials: BmcCredentials,
248 cache_settings: CacheSettings,
249 ) -> Self {
250 Self::with_custom_headers(
251 client,
252 redfish_endpoint,
253 credentials,
254 cache_settings,
255 HeaderMap::new(),
256 )
257 }
258
259 pub fn with_custom_headers(
311 client: C,
312 redfish_endpoint: Url,
313 credentials: BmcCredentials,
314 cache_settings: CacheSettings,
315 custom_headers: HeaderMap,
316 ) -> Self {
317 Self {
318 client,
319 redfish_endpoint: RedfishEndpoint::from(redfish_endpoint),
320 credentials: RwLock::new(Arc::new(credentials)),
321 cache: RwLock::new(TypeErasedCarCache::new(cache_settings.capacity)),
322 etags: RwLock::new(HashMap::new()),
323 custom_headers,
324 }
325 }
326
327 #[allow(clippy::panic)] pub fn set_credentials(&self, credentials: BmcCredentials) {
337 *self.credentials.write().expect("poisoned") = Arc::new(credentials);
338 }
339}
340
341#[derive(Debug, Clone)]
345pub struct RedfishEndpoint {
346 base_url: Url,
347}
348
349impl RedfishEndpoint {
350 #[must_use]
352 pub const fn new(base_url: Url) -> Self {
353 Self { base_url }
354 }
355
356 #[must_use]
358 pub fn with_path(&self, path: &str) -> Url {
359 let mut url = self.base_url.clone();
360 url.set_path(path);
361 url
362 }
363
364 #[must_use]
366 pub fn with_path_and_query(&self, path: &str, query: &str) -> Url {
367 let mut url = self.with_path(path);
368 url.set_query(Some(query));
369 url
370 }
371}
372
373#[derive(Clone, Copy)]
375pub struct CacheSettings {
376 capacity: usize,
377}
378
379impl Default for CacheSettings {
380 fn default() -> Self {
381 Self { capacity: 100 }
382 }
383}
384
385impl CacheSettings {
386 #[must_use]
388 pub const fn with_capacity(capacity: usize) -> Self {
389 Self { capacity }
390 }
391}
392
393impl From<Url> for RedfishEndpoint {
394 fn from(url: Url) -> Self {
395 Self::new(url)
396 }
397}
398
399impl From<&RedfishEndpoint> for Url {
400 fn from(endpoint: &RedfishEndpoint) -> Self {
401 endpoint.base_url.clone()
402 }
403}
404
405pub trait CacheableError {
408 fn is_cached(&self) -> bool;
411
412 fn cache_miss() -> Self;
414
415 fn cache_error(reason: String) -> Self;
417}
418
419impl<C: HttpClient> HttpBmc<C>
420where
421 C::Error: CacheableError + StdError + Send + Sync,
422{
423 #[allow(clippy::panic)] fn read_credentials(&self) -> Arc<BmcCredentials> {
425 self.credentials
426 .read()
427 .map(|credentials| Arc::clone(&credentials))
428 .expect("lock poisoned")
429 }
430
431 #[allow(clippy::significant_drop_tightening)]
439 async fn get_with_cache<T: EntityTypeRef + for<'de> Deserialize<'de> + 'static>(
440 &self,
441 endpoint_url: Url,
442 id: &ODataId,
443 ) -> Result<Arc<T>, C::Error> {
444 let etag: Option<ODataETag> = {
446 let etags = self
447 .etags
448 .read()
449 .map_err(|e| C::Error::cache_error(e.to_string()))?;
450 etags.get(id).cloned()
451 };
452 let credentials = self.read_credentials();
453
454 match self
456 .client
457 .get::<T>(
458 endpoint_url,
459 credentials.as_ref(),
460 etag,
461 &self.custom_headers,
462 )
463 .await
464 {
465 Ok(response) => {
466 let entity = Arc::new(response);
467
468 if let Some(etag) = entity.etag() {
470 let mut cache = self
471 .cache
472 .write()
473 .map_err(|e| C::Error::cache_error(e.to_string()))?;
474
475 let mut etags = self
476 .etags
477 .write()
478 .map_err(|e| C::Error::cache_error(e.to_string()))?;
479
480 if let Some(evicted_id) = cache.put_typed(id.clone(), Arc::clone(&entity)) {
481 etags.remove(&evicted_id);
482 }
483 etags.insert(id.clone(), etag.clone());
484 }
485 Ok(entity)
486 }
487 Err(e) => {
488 if e.is_cached() {
490 let mut cache = self
491 .cache
492 .write()
493 .map_err(|e| C::Error::cache_error(e.to_string()))?;
494 cache
495 .get_typed::<Arc<T>>(id)
496 .cloned()
497 .ok_or_else(C::Error::cache_miss)
498 } else {
499 Err(e)
500 }
501 }
502 }
503 }
504}
505
506impl<C: HttpClient> Bmc for HttpBmc<C>
507where
508 C::Error: CacheableError + StdError + Send + Sync,
509{
510 type Error = C::Error;
511
512 async fn get<T: EntityTypeRef + for<'de> Deserialize<'de> + 'static>(
513 &self,
514 id: &ODataId,
515 ) -> Result<Arc<T>, Self::Error> {
516 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
517 self.get_with_cache(endpoint_url, id).await
518 }
519
520 async fn expand<T: Expandable + 'static>(
521 &self,
522 id: &ODataId,
523 query: ExpandQuery,
524 ) -> Result<Arc<T>, Self::Error> {
525 let endpoint_url = self
526 .redfish_endpoint
527 .with_path_and_query(&id.to_string(), &query.to_query_string());
528
529 self.get_with_cache(endpoint_url, id).await
530 }
531
532 async fn create<V: Sync + Send + Serialize, R: Sync + Send + for<'de> Deserialize<'de>>(
533 &self,
534 id: &ODataId,
535 v: &V,
536 ) -> Result<ModificationResponse<R>, Self::Error> {
537 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
538 let credentials = self.read_credentials();
539 self.client
540 .post(endpoint_url, v, credentials.as_ref(), &self.custom_headers)
541 .await
542 }
543
544 async fn create_session<
545 V: Sync + Send + Serialize,
546 R: Sync + Send + for<'de> Deserialize<'de>,
547 >(
548 &self,
549 id: &ODataId,
550 v: &V,
551 ) -> Result<SessionCreateResponse<R>, Self::Error> {
552 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
553 self.client
554 .post_session(endpoint_url, v, &self.custom_headers)
555 .await
556 }
557
558 async fn update<V: Sync + Send + Serialize, R: Sync + Send + for<'de> Deserialize<'de>>(
559 &self,
560 id: &ODataId,
561 etag: Option<&ODataETag>,
562 v: &V,
563 ) -> Result<ModificationResponse<R>, Self::Error> {
564 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
565 let etag = etag
566 .cloned()
567 .unwrap_or_else(|| ODataETag::from(String::from("*")));
568 let credentials = self.read_credentials();
569 self.client
570 .patch(
571 endpoint_url,
572 etag,
573 v,
574 credentials.as_ref(),
575 &self.custom_headers,
576 )
577 .await
578 }
579
580 async fn delete<T: Sync + Send + for<'de> Deserialize<'de>>(
581 &self,
582 id: &ODataId,
583 ) -> Result<ModificationResponse<T>, Self::Error> {
584 let endpoint_url = self.redfish_endpoint.with_path(&id.to_string());
585 let credentials = self.read_credentials();
586 self.client
587 .delete(endpoint_url, credentials.as_ref(), &self.custom_headers)
588 .await
589 }
590
591 async fn action<T: Send + Sync + Serialize, R: Send + Sync + for<'de> Deserialize<'de>>(
592 &self,
593 action: &Action<T, R>,
594 params: &T,
595 ) -> Result<ModificationResponse<R>, Self::Error> {
596 let endpoint_url = self.redfish_endpoint.with_path(&action.target.to_string());
597 let credentials = self.read_credentials();
598 self.client
599 .post(
600 endpoint_url,
601 params,
602 credentials.as_ref(),
603 &self.custom_headers,
604 )
605 .await
606 }
607
608 async fn multipart_update<U, V, R>(
609 &self,
610 uri: &str,
611 request: MultipartUpdateRequest<'_, U, V>,
612 ) -> Result<ModificationResponse<R>, Self::Error>
613 where
614 U: UploadReader,
615 R: Send + Sync + for<'de> Deserialize<'de>,
616 V: Send + Sync + Serialize,
617 {
618 let endpoint_url = Url::parse(uri).unwrap_or_else(|_| self.redfish_endpoint.with_path(uri));
621 let credentials = self.read_credentials();
622
623 self.client
624 .post_multipart_update(
625 endpoint_url,
626 request,
627 credentials.as_ref(),
628 &self.custom_headers,
629 )
630 .await
631 }
632
633 #[cfg(feature = "update-service-deprecated")]
634 async fn http_push_uri_update<U, R>(
635 &self,
636 uri: &str,
637 request: HttpPushUriUpdateRequest<U>,
638 ) -> Result<ModificationResponse<R>, Self::Error>
639 where
640 U: UploadReader,
641 R: Send + Sync + for<'de> Deserialize<'de>,
642 {
643 let endpoint_url = Url::parse(uri).unwrap_or_else(|_| self.redfish_endpoint.with_path(uri));
646 let credentials = self.read_credentials();
647
648 self.client
649 .post_http_push_uri_update(
650 endpoint_url,
651 request,
652 credentials.as_ref(),
653 &self.custom_headers,
654 )
655 .await
656 }
657
658 async fn filter<T: EntityTypeRef + for<'de> Deserialize<'de> + 'static>(
659 &self,
660 id: &ODataId,
661 query: FilterQuery,
662 ) -> Result<Arc<T>, Self::Error> {
663 let endpoint_url = self
664 .redfish_endpoint
665 .with_path_and_query(&id.to_string(), &query.to_query_string());
666
667 self.get_with_cache(endpoint_url, id).await
668 }
669
670 async fn stream<T: Send + Sized + for<'de> Deserialize<'de>>(
671 &self,
672 uri: &str,
673 ) -> Result<BoxTryStream<T, Self::Error>, Self::Error> {
674 let endpoint_url = Url::parse(uri).unwrap_or_else(|_| self.redfish_endpoint.with_path(uri));
675 let credentials = self.read_credentials();
676 self.client
677 .sse(endpoint_url, credentials.as_ref(), &self.custom_headers)
678 .await
679 }
680}