Skip to main content

openstack_sdk/
openstack_async.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12//
13// SPDX-License-Identifier: Apache-2.0
14
15//! Asynchronous OpenStack client
16
17use std::collections::HashMap;
18use std::convert::TryInto;
19use std::fmt::{self, Debug};
20use std::time::{Duration, SystemTime};
21use std::{fs::File, io::Read};
22
23use async_trait::async_trait;
24use bytes::Bytes;
25use chrono::TimeDelta;
26use futures::io::{Error as IoError, ErrorKind as IoErrorKind};
27use futures::stream::TryStreamExt;
28use http::{HeaderMap, HeaderValue, Response as HttpResponse, StatusCode, header};
29use reqwest::{Body, Certificate, Client as AsyncClient, Request, Response};
30use secrecy::{ExposeSecret, SecretString};
31use tokio_util::codec;
32use tokio_util::compat::FuturesAsyncReadCompatExt;
33use tracing::{Level, debug, enabled, error, event, info, instrument, trace, warn};
34
35use openstack_sdk_auth_core::{
36    Auth, AuthError, AuthPluginRegistration, AuthToken, OpenStackAuthType,
37    authtoken::AuthTokenError,
38    authtoken_scope::AuthTokenScope,
39    types::{AuthResponse, Project, ServiceEndpoints},
40};
41
42// Force the linker to include crate plugins
43use openstack_sdk_auth_applicationcredential as _;
44#[cfg(feature = "keystone_ng")]
45use openstack_sdk_auth_federation as _;
46#[cfg(feature = "keystone_ng")]
47use openstack_sdk_auth_jwt as _;
48use openstack_sdk_auth_oidcaccesstoken as _;
49#[cfg(feature = "passkey")]
50use openstack_sdk_auth_passkey as _;
51use openstack_sdk_auth_password as _;
52use openstack_sdk_auth_receipt as token_receipt;
53use openstack_sdk_auth_token as token_auth;
54use openstack_sdk_auth_totp as _;
55use openstack_sdk_auth_websso as _;
56
57use crate::auth::authtoken::{AuthType, build_token_info_endpoint};
58
59use openstack_sdk_core::api::{
60    self, RestClient,
61    query::{self, RawQueryAsync},
62};
63use openstack_sdk_core::auth::{
64    AuthState,
65    auth_helper::{AuthHelper, Dialoguer, Noop},
66    gather_auth_data,
67};
68use openstack_sdk_core::catalog::{Catalog, CatalogError, ServiceEndpoint};
69use openstack_sdk_core::config::{CloudConfig, ConfigFile, get_config_identity_hash};
70use openstack_sdk_core::error::{OpenStackError, OpenStackResult, RestError};
71use openstack_sdk_core::state;
72use openstack_sdk_core::types::{ApiVersion, BoxedAsyncRead, ServiceType};
73use openstack_sdk_core::utils::expand_tilde;
74
75/// Asynchronous client for the OpenStack API for a single user
76///
77/// Separate Identity (not the scope) should use separate instances of this.
78/// ```rust
79/// use openstack_sdk::api::{paged, Pagination, QueryAsync};
80/// use openstack_sdk::{AsyncOpenStack, config::ConfigFile, OpenStackError};
81/// use openstack_sdk::types::ServiceType;
82/// use openstack_sdk::api::compute::v2::flavor::list;
83///
84/// async fn list_flavors() -> Result<(), OpenStackError> {
85///     // Get the builder for the listing Flavors Endpoint
86///     let mut ep_builder = list::Request::builder();
87///     // Set the `min_disk` query param
88///     ep_builder.min_disk("15");
89///     let ep = ep_builder.build().unwrap();
90///
91///     let cfg = ConfigFile::new().unwrap();
92///     // Get connection config from clouds.yaml/secure.yaml
93///     let profile = cfg.get_cloud_config("devstack").unwrap().unwrap();
94///     // Establish connection
95///     let mut session = AsyncOpenStack::new(&profile).await?;
96///
97///     // Invoke service discovery when desired.
98///     session.discover_service_endpoint(&ServiceType::Compute).await?;
99///
100///     // Execute the call with pagination limiting maximum amount of entries to 1000
101///     let data: Vec<serde_json::Value> = paged(ep, Pagination::Limit(1000))
102///         .query_async(&session)
103///         .await.unwrap();
104///
105///     println!("Data = {:?}", data);
106///     Ok(())
107/// }
108/// ```
109#[derive(Clone)]
110pub struct AsyncOpenStack {
111    /// The client to use for API calls.
112    client: reqwest::Client,
113    /// Cloud configuration
114    config: CloudConfig,
115    /// The authentication information to use when communicating with OpenStack.
116    auth: Auth,
117    /// Endpoints catalog
118    catalog: Catalog,
119    /// Session state.
120    ///
121    /// In order to save authentication roundtrips save/load authentication
122    /// information in the file (similar to how other cli tools are doing)
123    /// and check auth expiration upon load.
124    state: state::State,
125}
126
127impl Debug for AsyncOpenStack {
128    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129        f.debug_struct("OpenStack")
130            .field("service_endpoints", &self.catalog)
131            .finish()
132    }
133}
134
135#[async_trait]
136impl api::RestClient for AsyncOpenStack {
137    type Error = RestError;
138
139    /// Get service endpoint from the catalog
140    fn get_service_endpoint(
141        &self,
142        service_type: &ServiceType,
143        version: Option<&ApiVersion>,
144    ) -> Result<&ServiceEndpoint, api::ApiError<Self::Error>> {
145        Ok(self.catalog.get_service_endpoint(
146            service_type.to_string(),
147            version,
148            self.config.region_name.as_ref(),
149            self.config.interface.as_ref(),
150        )?)
151    }
152
153    /// Get project id from the current scope
154    fn get_current_project(&self) -> Option<Project> {
155        if let Auth::AuthToken(token) = &self.auth {
156            return token.auth_info.clone().and_then(|x| x.token.project);
157        }
158        None
159    }
160}
161
162#[async_trait]
163impl api::AsyncClient for AsyncOpenStack {
164    // Perform REST request
165    async fn rest_async(
166        &self,
167        request: http::request::Builder,
168        body: Vec<u8>,
169    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
170        self.rest_with_auth_async(request, body, &self.auth).await
171    }
172
173    /// Perform REST request with the body read from AsyncRead
174    async fn rest_read_body_async(
175        &self,
176        request: http::request::Builder,
177        body: BoxedAsyncRead,
178    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
179        self.rest_with_auth_read_body_async(request, body, &self.auth)
180            .await
181    }
182
183    /// Download result of HTTP operation.
184    async fn download_async(
185        &self,
186        request: http::request::Builder,
187        body: Vec<u8>,
188    ) -> Result<(HeaderMap, BoxedAsyncRead), api::ApiError<<Self as api::RestClient>::Error>> {
189        self.download_with_auth_async(request, body, &self.auth)
190            .await
191    }
192}
193
194impl AsyncOpenStack {
195    /// Basic constructor
196    fn new_impl(config: &CloudConfig, auth: Auth) -> OpenStackResult<Self> {
197        let mut client_builder = AsyncClient::builder();
198
199        if let Some(cacert) = &config.cacert {
200            let mut buf = Vec::new();
201            File::open(expand_tilde(cacert).unwrap_or(cacert.into()))
202                .map_err(|e| OpenStackError::IOWithPath {
203                    source: e,
204                    path: cacert.into(),
205                })?
206                .read_to_end(&mut buf)
207                .map_err(|e| OpenStackError::IOWithPath {
208                    source: e,
209                    path: cacert.into(),
210                })?;
211            for cert in Certificate::from_pem_bundle(&buf)? {
212                client_builder = client_builder.add_root_certificate(cert);
213            }
214        }
215        if let Some(false) = &config.verify {
216            warn!(
217                "SSL Verification is disabled! Please consider using `cacert` for adding custom certificate instead."
218            );
219            client_builder = client_builder.danger_accept_invalid_certs(true);
220        }
221        client_builder = client_builder.pool_max_idle_per_host(10);
222        client_builder = client_builder.pool_idle_timeout(Duration::from_secs(30));
223        client_builder = client_builder.timeout(Duration::from_secs(
224            config
225                .options
226                .get("api_timeout")
227                .and_then(|val| val.clone().into_uint().ok())
228                .unwrap_or(30),
229        ));
230        client_builder = client_builder.connect_timeout(Duration::from_secs(5));
231        client_builder = client_builder.tcp_keepalive(Duration::from_secs(60));
232        client_builder = client_builder.gzip(true);
233        client_builder = client_builder.deflate(true);
234
235        let mut session = AsyncOpenStack {
236            client: client_builder.build()?,
237            config: config.clone(),
238            auth,
239            catalog: Catalog::default(),
240            state: state::State::new(),
241        };
242
243        let auth_data = session
244            .config
245            .auth
246            .as_ref()
247            .ok_or(AuthTokenError::MissingAuthData)?;
248
249        let identity_service_url = auth_data
250            .auth_url
251            .as_ref()
252            .ok_or(AuthTokenError::MissingAuthUrl)?;
253
254        session.catalog.register_catalog_endpoint(
255            "identity",
256            identity_service_url,
257            config.region_name.as_ref(),
258            Some("public"),
259        )?;
260
261        session.catalog.configure(config)?;
262
263        session
264            .state
265            .set_auth_hash_key(get_config_identity_hash(config))
266            .enable_auth_cache(ConfigFile::new()?.is_auth_cache_enabled());
267
268        Ok(session)
269    }
270
271    /// Create a new OpenStack API session from CloudConfig
272    #[instrument(name = "connect", level = "trace", skip(config))]
273    pub async fn new(config: &CloudConfig) -> OpenStackResult<Self> {
274        let mut session = Self::new_impl(config, Auth::None)?;
275
276        // Ensure we resolve identity endpoint using version discovery
277        session
278            .discover_service_endpoint(&ServiceType::Identity)
279            .await?;
280
281        session.authorize(None, false, false).await?;
282
283        Ok(session)
284    }
285
286    /// Create a new OpenStack API session from CloudConfig
287    #[instrument(name = "connect", level = "trace", skip(config, auth_helper))]
288    pub async fn new_with_authentication_helper<A>(
289        config: &CloudConfig,
290        auth_helper: &mut A,
291        renew_auth: bool,
292    ) -> OpenStackResult<Self>
293    where
294        A: AuthHelper + Sync + Send,
295    {
296        let mut session = Self::new_impl(config, Auth::None)?;
297
298        // Ensure we resolve identity endpoint using version discovery
299        session
300            .discover_service_endpoint(&ServiceType::Identity)
301            .await?;
302
303        session
304            .authorize_with_auth_helper(None, auth_helper, renew_auth)
305            .await?;
306
307        Ok(session)
308    }
309
310    /// Create a new OpenStack API session from CloudConfig
311    #[instrument(name = "connect", level = "trace", skip(config))]
312    #[deprecated(
313        since = "0.22.0",
314        note = "please use `new_with_authentication_helper` instead"
315    )]
316    pub async fn new_interactive(config: &CloudConfig, renew_auth: bool) -> OpenStackResult<Self> {
317        Self::new_with_authentication_helper(config, &mut Dialoguer::default(), renew_auth).await
318    }
319
320    /// Set the authorization to be used by the client
321    fn set_auth(&mut self, auth: Auth, skip_cache_update: bool) -> &mut Self {
322        self.auth = auth;
323        if !skip_cache_update && let Auth::AuthToken(auth) = &self.auth {
324            // For app creds we should save auth as unscoped since:
325            // - on request it is disallowed to specify scope
326            // - response contain fixed scope
327            // With this it is not possible to find auth in the cache if we use the real
328            // scope
329            let scope = match &auth.auth_info {
330                Some(info) => {
331                    if info.token.application_credential.is_some() {
332                        AuthTokenScope::Unscoped
333                    } else {
334                        auth.get_scope()
335                    }
336                }
337                _ => auth.get_scope(),
338            };
339            self.state.set_scope_auth(&scope, auth);
340        }
341        self
342    }
343
344    /// Authorize against the cloud using provided credentials and get the session token.
345    pub async fn authorize(
346        &mut self,
347        scope: Option<AuthTokenScope>,
348        interactive: bool,
349        renew_auth: bool,
350    ) -> Result<(), OpenStackError> {
351        if interactive {
352            self.authorize_with_auth_helper(scope, &mut Dialoguer::default(), renew_auth)
353                .await
354        } else {
355            self.authorize_with_auth_helper(scope, &mut Noop::default(), renew_auth)
356                .await
357        }
358    }
359
360    /// Re-authenticate with the existing auth for the given scope.
361    async fn reauth(
362        &self,
363        auth: &AuthToken,
364        scope: &AuthTokenScope,
365    ) -> Result<Auth, OpenStackError> {
366        Ok(token_auth::PLUGIN
367            .auth(
368                &self.client,
369                self.get_service_endpoint(&ServiceType::Identity, Some(&ApiVersion::from((3, 0))))?
370                    .url(),
371                HashMap::from([("token".into(), auth.token.clone())]),
372                Some(scope),
373                None,
374            )
375            .await?)
376    }
377
378    /// Authorize against the cloud using provided credentials and get the session token with the
379    /// auth helper that may be invoked to interactively ask for the credentials.
380    pub async fn authorize_with_auth_helper<A>(
381        &mut self,
382        scope: Option<AuthTokenScope>,
383        auth_helper: &mut A,
384        renew_auth: bool,
385    ) -> Result<(), OpenStackError>
386    where
387        A: AuthHelper + Sync + Send,
388    {
389        let requested_scope =
390            scope.map_or_else(|| AuthTokenScope::try_from(&self.config), |v| Ok(v.clone()))?;
391
392        if let (Some(auth), false) = (self.state.get_scope_auth(&requested_scope), renew_auth) {
393            // Valid authorization is already available and no renewal is required
394            trace!("Auth already available");
395            self.set_auth(Auth::AuthToken(Box::new(auth.clone())), true);
396        } else {
397            // No valid authorization data is available in the state or
398            // renewal is requested
399            let auth_type = AuthType::from_cloud_config(&self.config)?;
400            let mut force_new_auth = renew_auth;
401            if let AuthType::V3ApplicationCredential = auth_type {
402                // application_credentials token can not be used to get new token without again
403                // supplying application credentials.
404                force_new_auth = true;
405            }
406            if let (Some(available_auth), false) = (self.state.get_any_valid_auth(), force_new_auth)
407            {
408                // State contain valid authentication for different scope/unscoped. It is possible
409                // to request new authz using this other auth
410                trace!("Valid Auth is available for reauthz: {:?}", available_auth);
411                let token_auth = self.reauth(&available_auth, &requested_scope).await?;
412                self.set_auth(token_auth.clone(), false);
413            } else {
414                // No auth/authz information available. Proceed with new auth
415                trace!("No Auth already available. Proceeding with new login");
416
417                let auth_type = auth_type.as_str();
418                // Find authenticator supporting the auth_type
419                if let Some(authenticator) = inventory::iter::<AuthPluginRegistration>
420                    .into_iter()
421                    .find(|x| x.method.get_supported_auth_methods().contains(&auth_type))
422                    .map(|x| x.method)
423                {
424                    // authenticate
425                    let auth_hints = self
426                        .config
427                        .auth_methods
428                        .as_ref()
429                        .map(|methods| serde_json::json!({"auth_methods": methods}));
430                    match authenticator
431                        .auth(
432                            &self.client,
433                            self.get_service_endpoint(
434                                &ServiceType::Identity,
435                                Some(&ApiVersion::from(authenticator.api_version())),
436                            )?
437                            .url(),
438                            gather_auth_data(
439                                &authenticator.requirements(auth_hints.as_ref())?,
440                                &self.config,
441                                auth_helper,
442                            )
443                            .await?,
444                            Some(&requested_scope),
445                            auth_hints.as_ref(),
446                        )
447                        .await
448                    {
449                        Ok(token_auth) => {
450                            self.set_auth(token_auth.clone(), false);
451                        }
452                        Err(AuthError::AuthReceipt(receipt)) => {
453                            // Auth Receipt is received
454                            // Find the receipt auth plugin
455                            // Convert the receipt into auth hints
456                            let auth_hints = serde_json::to_value(&receipt)?;
457                            // Authenticate
458                            let token_auth = token_receipt::PLUGIN
459                                .auth(
460                                    &self.client,
461                                    self.get_service_endpoint(
462                                        &ServiceType::Identity,
463                                        Some(&ApiVersion::from(authenticator.api_version())),
464                                    )?
465                                    .url(),
466                                    gather_auth_data(
467                                        &token_receipt::PLUGIN.requirements(Some(&auth_hints))?,
468                                        &self.config,
469                                        auth_helper,
470                                    )
471                                    .await?,
472                                    Some(&requested_scope),
473                                    Some(&auth_hints),
474                                )
475                                .await?;
476                            self.set_auth(token_auth.clone(), false);
477                        }
478                        Err(other) => {
479                            return Err(other.into());
480                        }
481                    }
482                } else {
483                    return Err(AuthTokenError::IdentityMethod {
484                        auth_type: auth_type.into(),
485                    })?;
486                }
487            }
488        }
489
490        if let Auth::AuthToken(token_auth) = &self.auth {
491            // When unscoped auth is requested (maybe by not specifying the scope) we expect that
492            // the Authenticator returns the necessary scope (i.e. for ApplicationCredentials it is
493            // not possible to request scope, but the Keystone manages the scope) or unscoped.
494            // In this case we just save the auth as unscoped (in addition to what was done above).
495            // Otherwise we should rescope.
496
497            if token_auth.auth_info.is_none() {
498                let mut resolved_token = token_auth.clone();
499                let token_info = self.fetch_token_info(token_auth.token.clone()).await?;
500                resolved_token.auth_info = Some(token_info.clone());
501                let scope = AuthTokenScope::from(&token_info);
502
503                // Save unscoped token in the cache
504                self.state.set_scope_auth(&scope, &resolved_token);
505            }
506
507            if requested_scope != AuthTokenScope::Unscoped
508                && !token_auth
509                    .auth_info
510                    .as_ref()
511                    .map(AuthTokenScope::from)
512                    .is_some_and(|scope| requested_scope == scope)
513            {
514                // And now time to rescope the token
515                let token_auth = self.reauth(token_auth, &requested_scope).await?;
516                //let auth_ep = build_reauth_request(token_auth, &requested_scope)?;
517                //let rsp = auth_ep.raw_query_async(self).await?;
518                //let token_auth = Auth::try_from(rsp)?;
519                self.set_auth(token_auth.clone(), false);
520            } else {
521                // Client may not specify the target scope expecting the mapping to set
522                // the proper token. Save the auth as unscope (similarly to the AppCred
523                // handling).
524                self.state
525                    .set_scope_auth(&AuthTokenScope::Unscoped, token_auth);
526            }
527        } else {
528            return Err(AuthError::AuthTokenNotInResponse)?;
529        }
530
531        if let Auth::AuthToken(token_data) = &self.auth {
532            match &token_data.auth_info {
533                Some(auth_data) => {
534                    if let Some(project) = &auth_data.token.project {
535                        self.catalog.set_project_id(project.id.clone());
536                        // Reconfigure catalog since we know now the project_id
537                        self.catalog.configure(&self.config)?;
538                    }
539                    if let Some(endpoints) = &auth_data.token.catalog {
540                        self.catalog.process_catalog_endpoints(endpoints)?;
541                    } else {
542                        error!("No catalog information");
543                    }
544                }
545                _ => return Err(OpenStackError::NoAuth),
546            }
547        }
548        // TODO: without AuthToken authorization we may want to read catalog separately
549        Ok(())
550    }
551
552    /// Perform version discovery of a service
553    #[instrument(skip(self))]
554    pub async fn discover_service_endpoint(
555        &mut self,
556        service_type: &ServiceType,
557    ) -> Result<(), OpenStackError> {
558        if let Ok(ep) = self.catalog.get_service_endpoint(
559            service_type.to_string(),
560            None,
561            self.config.region_name.as_ref(),
562            self.config.interface.as_ref(),
563        ) {
564            if self.catalog.discovery_allowed(service_type.to_string()) {
565                info!("Performing `{}` endpoint version discovery", service_type);
566
567                let orig_url = ep.url().clone();
568                let mut try_url = ep.url().clone();
569                // Version discovery document must logically end with "/" since API url goes even
570                // deeper.
571                try_url
572                    .path_segments_mut()
573                    .map_err(|_| CatalogError::cannot_be_base(ep.url()))?
574                    .pop_if_empty()
575                    .push("");
576                let mut max_depth = 10;
577                loop {
578                    let req = http::Request::builder()
579                        .method(http::Method::GET)
580                        .uri(query::url_to_http_uri(try_url.clone())?);
581
582                    match self.rest_with_auth_async(req, Vec::new(), &self.auth).await {
583                        Ok(rsp) => {
584                            if rsp.status() != StatusCode::NOT_FOUND
585                                && self
586                                    .catalog
587                                    .process_endpoint_discovery(
588                                        service_type,
589                                        &try_url,
590                                        rsp.body(),
591                                        self.config.region_name.as_ref(),
592                                        self.config.interface.as_ref(),
593                                    )
594                                    .is_ok()
595                            {
596                                debug!(
597                                    "Finished service version discovery at {}",
598                                    try_url.as_str()
599                                );
600                                debug!("catalog {:?}", self.catalog);
601                                return Ok(());
602                            }
603                        }
604                        Err(err) => {
605                            error!(
606                                "Error querying {} for the version discovery. It is most likely a misconfiguration on the cloud side. {}",
607                                try_url.as_str(),
608                                err
609                            );
610                        }
611                    };
612                    if try_url.path() != "/" {
613                        // We are not at the root yet and have not found a
614                        // valid version document so far, try one level up
615                        try_url
616                            .path_segments_mut()
617                            .map_err(|_| CatalogError::cannot_be_base(&orig_url))?
618                            .pop();
619                    } else {
620                        return Err(OpenStackError::Discovery {
621                            service: service_type.to_string(),
622                            url: orig_url.into(),
623                            msg: match service_type {
624                                ServiceType::Identity => "Service is not working.".into(),
625                                _ => "No Version document found. Either service is not supporting version discovery, or API is not working".into(),
626                            }
627                        });
628                    }
629
630                    max_depth -= 1;
631                    if max_depth == 0 {
632                        break;
633                    }
634                }
635                return Err(OpenStackError::Discovery {
636                    service: service_type.to_string(),
637                    url: orig_url.into(),
638                    msg: "Unknown".into(),
639                });
640            }
641            return Ok(());
642        }
643        Ok(())
644    }
645
646    // TODO(gtema): rename to `get_catalog`)
647    /// Return catalog information given in the token
648    pub fn get_token_catalog(&self) -> Option<Vec<ServiceEndpoints>> {
649        self.catalog.get_token_catalog()
650    }
651
652    /// Return current authentication information
653    pub fn get_auth_info(&self) -> Option<AuthResponse> {
654        if let Auth::AuthToken(token) = &self.auth {
655            return token.auth_info.clone();
656        }
657        None
658    }
659
660    /// Return current authentication status
661    ///
662    /// Offset can be used to calculate imminent expiration.
663    pub fn get_auth_state(&self, offset: Option<TimeDelta>) -> Option<AuthState> {
664        if let Auth::AuthToken(token) = &self.auth {
665            return Some(token.get_state(offset));
666        }
667        None
668    }
669
670    /// Return current authentication token
671    pub fn get_auth_token(&self) -> Option<SecretString> {
672        if let Auth::AuthToken(token) = &self.auth {
673            return Some(token.token.clone());
674        }
675        None
676    }
677
678    /// Perform token introspection call
679    pub async fn fetch_token_info(
680        &self,
681        token: SecretString,
682    ) -> Result<AuthResponse, OpenStackError> {
683        let auth_ep = build_token_info_endpoint(token.expose_secret())?;
684        let rsp = auth_ep.raw_query_async(self).await?;
685        let data: AuthResponse = serde_json::from_slice(rsp.body())?;
686        Ok(data)
687    }
688
689    /// Perform HTTP request with given request and return raw response.
690    #[instrument(name="request", skip_all, fields(http.uri = request.url().as_str(), http.method = request.method().as_str(), openstack.ver=request.headers().get("openstack-api-version").map(|v| v.to_str().unwrap_or(""))))]
691    async fn execute_request(&self, request: Request) -> Result<Response, reqwest::Error> {
692        info!("Sending request {:?}", request);
693        let url = request.url().clone();
694        let method = request.method().clone();
695
696        if enabled!(Level::TRACE)
697            && request.headers().get(header::CONTENT_TYPE)
698                == Some(&HeaderValue::from_static("application/json"))
699        {
700            // Body may contain sensitive info - censor it but only when trace is on
701            request
702                .body()
703                .and_then(|body| body.as_bytes())
704                .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok())
705                .inspect(|rq| {
706                    let censored = self
707                        .config
708                        .get_sensitive_values()
709                        .iter()
710                        .fold(rq.clone(), |sanitized, &secret| {
711                            sanitized.replace(secret, "<CENSORED>")
712                        });
713                    trace!("Request Body: {:?}", censored);
714                });
715        }
716
717        let start = SystemTime::now();
718        let rsp = self.client.execute(request).await?;
719        let elapsed = SystemTime::now().duration_since(start).unwrap_or_default();
720        event!(
721            name: "http_request",
722            Level::INFO,
723            url=url.as_str(),
724            duration_ms=elapsed.as_millis(),
725            status=rsp.status().as_u16(),
726            method=method.as_str(),
727            request_id=rsp.headers().get("x-openstack-request-id").map(|v| v.to_str().unwrap_or("")),
728            "Request completed with status {}",
729            rsp.status(),
730        );
731        Ok(rsp)
732    }
733
734    /// Perform a REST query with a given auth.
735    async fn rest_with_auth_async(
736        &self,
737        mut request: http::request::Builder,
738        body: Vec<u8>,
739        auth: &Auth,
740    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
741        use futures_util::TryFutureExt;
742        let call = || async {
743            if let Some(headers) = request.headers_mut() {
744                auth.set_header(headers)?;
745            }
746            let http_request = request.body(body)?;
747            let request = http_request.try_into()?;
748
749            let rsp = self.execute_request(request).await?;
750
751            let mut http_rsp = HttpResponse::builder()
752                .status(rsp.status())
753                .version(rsp.version());
754
755            if let Some(headers) = http_rsp.headers_mut() {
756                headers.extend(rsp.headers().clone())
757            }
758
759            Ok(http_rsp.body(rsp.bytes().await?)?)
760        };
761        call().map_err(api::ApiError::client).await
762    }
763
764    /// Perform a REST query with a given auth.
765    async fn rest_with_auth_read_body_async(
766        &self,
767        mut request: http::request::Builder,
768        body_read: BoxedAsyncRead,
769        auth: &Auth,
770    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
771        use futures_util::TryFutureExt;
772        let call = || async {
773            if let Some(headers) = request.headers_mut() {
774                auth.set_header(headers)?;
775            }
776            let stream = codec::FramedRead::new(body_read.compat(), codec::BytesCodec::new())
777                .map_ok(|b| b.freeze());
778            let http_request = request.body(Body::wrap_stream(stream))?;
779            let request = http_request.try_into()?;
780
781            let rsp = self.execute_request(request).await?;
782
783            let mut http_rsp = HttpResponse::builder()
784                .status(rsp.status())
785                .version(rsp.version());
786
787            if let Some(headers) = http_rsp.headers_mut() {
788                headers.extend(rsp.headers().clone())
789            }
790
791            Ok(http_rsp.body(rsp.bytes().await?)?)
792        };
793        call().map_err(api::ApiError::client).await
794    }
795
796    /// Perform a REST query with a given auth and return AsyncRead of the body.
797    async fn download_with_auth_async(
798        &self,
799        mut request: http::request::Builder,
800        body: Vec<u8>,
801        auth: &Auth,
802    ) -> Result<(HeaderMap, BoxedAsyncRead), api::ApiError<<Self as api::RestClient>::Error>> {
803        use futures_util::TryFutureExt;
804        let call = || async {
805            if let Some(headers) = request.headers_mut() {
806                auth.set_header(headers)?;
807            }
808            let http_request = request.body(body)?;
809            let request = http_request.try_into()?;
810            let rsp = self.execute_request(request).await?;
811
812            let mut headers = HeaderMap::new();
813            for (key, value) in rsp.headers() {
814                headers.insert(key, value.clone());
815            }
816
817            let boxed_async_read = BoxedAsyncRead::new(
818                rsp.bytes_stream()
819                    .map_err(|orig| {
820                        let kind = if orig.is_timeout() {
821                            IoErrorKind::TimedOut
822                        } else {
823                            IoErrorKind::Other
824                        };
825                        IoError::new(kind, orig)
826                    })
827                    .into_async_read(),
828            );
829            Ok((headers, boxed_async_read))
830        };
831        call().map_err(api::ApiError::client).await
832    }
833}