openstack_sdk/
openstack.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//! Synchronous OpenStack client
16
17#![deny(dead_code, unused_imports, unused_mut)]
18
19use std::convert::TryInto;
20use std::fmt::{self, Debug};
21use std::time::SystemTime;
22use std::{fs::File, io::Read};
23use tracing::{debug, error, event, info, instrument, trace, warn, Level};
24
25use bytes::Bytes;
26use chrono::TimeDelta;
27use http::{Response as HttpResponse, StatusCode};
28
29use reqwest::{
30    blocking::{Client, Request, Response},
31    Certificate, Url,
32};
33
34use crate::config::CloudConfig;
35
36use crate::api;
37use crate::api::query;
38use crate::api::query::RawQuery;
39use crate::auth::{
40    self, authtoken,
41    authtoken::{AuthTokenError, AuthType},
42    Auth, AuthError, AuthState,
43};
44use crate::config::{get_config_identity_hash, ConfigFile};
45use crate::state;
46use crate::types::identity::v3::{AuthReceiptResponse, AuthResponse, Project};
47use crate::types::{ApiVersion, ServiceType};
48
49use crate::catalog::{Catalog, ServiceEndpoint};
50
51use crate::error::{OpenStackError, OpenStackResult, RestError};
52use crate::utils::expand_tilde;
53
54// Private enum that enables the parsing of the cert bytes to be
55// delayed until the client is built rather than when they're passed
56// to a builder.
57#[allow(dead_code)]
58#[derive(Clone)]
59enum ClientCert {
60    None,
61    #[cfg(feature = "client_der")]
62    Der(Vec<u8>, String),
63    #[cfg(feature = "client_pem")]
64    Pem(Vec<u8>),
65}
66
67/// Synchronous client for the OpenStack API for a single user.
68///
69/// Separate Identity (not the scope) should use separate instances of this.
70/// ```rust
71/// use openstack_sdk::api::{paged, Pagination, Query};
72/// use openstack_sdk::{OpenStack, config::ConfigFile, OpenStackError};
73/// use openstack_sdk::types::ServiceType;
74/// use openstack_sdk::api::compute::v2::flavor::list;
75///
76/// fn list_flavors() -> Result<(), OpenStackError> {
77///     // Get the builder for the listing Flavors Endpoint
78///     let mut ep_builder = list::Request::builder();
79///     // Set the `min_disk` query param
80///     ep_builder.min_disk("15");
81///     let ep = ep_builder.build().unwrap();
82///
83///     let cfg = ConfigFile::new().unwrap();
84///     // Get connection config from clouds.yaml/secure.yaml
85///     let profile = cfg.get_cloud_config("devstack").unwrap().unwrap();
86///     // Establish connection
87///     let mut session = OpenStack::new(&profile)?;
88///
89///     // Invoke service discovery when desired.
90///     session.discover_service_endpoint(&ServiceType::Compute)?;
91///
92///     // Execute the call with pagination limiting maximum amount of entries to 1000
93///     let data: Vec<serde_json::Value> = paged(ep, Pagination::Limit(1000))
94///         .query(&session)
95///         .unwrap();
96///
97///     println!("Data = {:?}", data);
98///     Ok(())
99/// }
100/// ```
101
102#[derive(Clone)]
103pub struct OpenStack {
104    /// The client to use for API calls.
105    client: Client,
106    /// Cloud configuration
107    config: CloudConfig,
108    /// The authentication information to use when communicating with OpenStack.
109    auth: Auth,
110    /// Endpoints catalog
111    catalog: Catalog,
112    /// Session state.
113    ///
114    /// In order to save authentication roundtrips save/load authentication
115    /// information in the file (similar to how other cli tools are doing)
116    /// and check auth expiration upon load.
117    state: state::State,
118}
119
120impl Debug for OpenStack {
121    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
122        f.debug_struct("OpenStack")
123            .field("service_endpoints", &self.catalog)
124            .finish()
125    }
126}
127
128/// Should a certificate be validated in tls connections.
129/// The Insecure option is used for self-signed certificates.
130#[allow(dead_code)]
131#[derive(Debug, Clone)]
132enum CertPolicy {
133    Default,
134    Insecure,
135}
136
137impl OpenStack {
138    /// Basic constructor
139    fn new_impl(config: &CloudConfig, auth: Auth) -> OpenStackResult<Self> {
140        let mut client_builder = Client::builder();
141
142        if let Some(cacert) = &config.cacert {
143            let mut buf = Vec::new();
144            File::open(expand_tilde(cacert).unwrap_or(cacert.into()))
145                .map_err(|e| OpenStackError::IO {
146                    source: e,
147                    path: cacert.into(),
148                })?
149                .read_to_end(&mut buf)
150                .map_err(|e| OpenStackError::IO {
151                    source: e,
152                    path: cacert.into(),
153                })?;
154            for cert in Certificate::from_pem_bundle(&buf)? {
155                client_builder = client_builder.add_root_certificate(cert);
156            }
157        }
158        if let Some(false) = &config.verify {
159            warn!(
160                "SSL Verification is disabled! Please consider using `cacert` instead for adding custom certificate."
161            );
162            client_builder = client_builder.danger_accept_invalid_certs(true);
163        }
164
165        let mut session = OpenStack {
166            client: client_builder.build()?,
167            config: config.clone(),
168            auth,
169            catalog: Catalog::default(),
170            state: state::State::new(),
171        };
172
173        let auth_data = session
174            .config
175            .auth
176            .as_ref()
177            .ok_or(AuthTokenError::MissingAuthData)?;
178
179        let identity_service_url = auth_data
180            .auth_url
181            .as_ref()
182            .ok_or(AuthTokenError::MissingAuthUrl)?;
183
184        session.catalog.register_catalog_endpoint(
185            "identity",
186            identity_service_url,
187            config.region_name.as_ref(),
188            Some("public"),
189        )?;
190
191        session.catalog.configure(config)?;
192
193        session
194            .state
195            .set_auth_hash_key(get_config_identity_hash(config))
196            .enable_auth_cache(ConfigFile::new()?.is_auth_cache_enabled());
197
198        Ok(session)
199    }
200
201    /// Create a new OpenStack API session from CloudConfig
202    #[instrument(name = "connect", level = "trace", skip(config))]
203    pub fn new(config: &CloudConfig) -> OpenStackResult<Self> {
204        let mut session = Self::new_impl(config, Auth::None)?;
205
206        // Ensure we resolve identity endpoint using version discovery
207        session.discover_service_endpoint(&ServiceType::Identity)?;
208
209        session.authorize(None, false, false)?;
210
211        Ok(session)
212    }
213
214    /// Set the authorization to be used by the client
215    fn set_auth(&mut self, auth: auth::Auth, skip_cache_update: bool) -> &mut Self {
216        self.auth = auth;
217        if !skip_cache_update {
218            if let Auth::AuthToken(auth) = &self.auth {
219                // For app creds we should save auth as unscoped since:
220                // - on request it is disallowed to specify scope
221                // - response contain fixed scope
222                // With this it is not possible to find auth in the cache if we use the real
223                // scope
224                let scope = match &auth.auth_info {
225                    Some(info) => {
226                        if info.token.application_credential.is_some() {
227                            authtoken::AuthTokenScope::Unscoped
228                        } else {
229                            auth.get_scope()
230                        }
231                    }
232                    _ => auth.get_scope(),
233                };
234                self.state.set_scope_auth(&scope, auth);
235            }
236        }
237        self
238    }
239
240    /// Set TokenAuth as current authorization
241    fn set_token_auth(&mut self, token: String, token_info: Option<AuthResponse>) -> &mut Self {
242        let token_auth = authtoken::AuthToken {
243            token,
244            auth_info: token_info,
245        };
246        self.set_auth(auth::Auth::AuthToken(Box::new(token_auth.clone())), false);
247        self
248    }
249
250    /// Authorize against the cloud using provided credentials and get the session token
251    pub fn authorize(
252        &mut self,
253        scope: Option<authtoken::AuthTokenScope>,
254        interactive: bool,
255        renew_auth: bool,
256    ) -> Result<(), OpenStackError> {
257        let requested_scope = scope.unwrap_or(authtoken::AuthTokenScope::try_from(&self.config)?);
258
259        if let (Some(auth), false) = (self.state.get_scope_auth(&requested_scope), renew_auth) {
260            // Valid authorization is already available and no renewal is required
261            trace!("Auth already available");
262            self.auth = auth::Auth::AuthToken(Box::new(auth.clone()));
263        } else {
264            // No valid authorization data is available in the state or
265            // renewal is requested
266            let auth_type = AuthType::from_cloud_config(&self.config)?;
267            let mut force_new_auth = renew_auth;
268            if let AuthType::V3ApplicationCredential = auth_type {
269                // application_credentials token can not be used to get new token without again
270                // supplying application credentials (bug in Keystone?)
271                // So for AppCred we just force a brand new auth
272                force_new_auth = true;
273            }
274            let mut rsp;
275            if let (Some(available_auth), false) = (self.state.get_any_valid_auth(), force_new_auth)
276            {
277                // State contain valid authentication for different
278                // scope/unscoped. It is possible to request new authz
279                // using this other auth
280                trace!("Valid Auth is available for reauthz: {:?}", available_auth);
281                let auth_ep = authtoken::build_reauth_request(&available_auth, &requested_scope)?;
282                rsp = auth_ep.raw_query(self)?;
283            } else {
284                // No auth/authz information available. Proceed with new auth
285                trace!("No Auth already available. Proceeding with new login");
286
287                match AuthType::from_cloud_config(&self.config)? {
288                    AuthType::V3ApplicationCredential => {
289                        let identity =
290                            authtoken::build_identity_data_from_config(&self.config, interactive)?;
291                        let auth_ep = authtoken::build_auth_request_with_identity_and_scope(
292                            &identity,
293                            &authtoken::AuthTokenScope::Unscoped,
294                        )?;
295                        rsp = auth_ep.raw_query(self)?;
296                    }
297                    AuthType::V3Password
298                    | AuthType::V3Token
299                    | AuthType::V3Totp
300                    | AuthType::V3Multifactor => {
301                        let identity =
302                            authtoken::build_identity_data_from_config(&self.config, interactive)?;
303                        let auth_ep = authtoken::build_auth_request_with_identity_and_scope(
304                            &identity,
305                            &requested_scope,
306                        )?;
307                        rsp = auth_ep.raw_query(self)?;
308
309                        // Handle the MFA
310                        if let StatusCode::UNAUTHORIZED = rsp.status() {
311                            if let Some(receipt) = rsp.headers().get("openstack-auth-receipt") {
312                                let receipt_data: AuthReceiptResponse =
313                                    serde_json::from_slice(rsp.body())
314                                        .expect("A valid OpenStack Auth receipt body");
315                                let auth_endpoint = authtoken::build_auth_request_from_receipt(
316                                    &self.config,
317                                    receipt.clone(),
318                                    &receipt_data,
319                                    &requested_scope,
320                                    interactive,
321                                )?;
322                                rsp = auth_endpoint.raw_query(self)?;
323                            }
324                        }
325                        api::check_response_error::<Self>(&rsp, None)?;
326                    }
327                    other => {
328                        return Err(AuthTokenError::IdentityMethodSync {
329                            auth_type: other.as_str().into(),
330                        })?;
331                    }
332                }
333            };
334
335            let data: AuthResponse = serde_json::from_slice(rsp.body())?;
336            debug!("Auth token is {:?}", data);
337
338            let token = rsp
339                .headers()
340                .get("x-subject-token")
341                .ok_or(AuthError::AuthTokenNotInResponse)?
342                .to_str()
343                .expect("x-subject-token is a string");
344
345            self.set_token_auth(token.into(), Some(data));
346        }
347
348        if let auth::Auth::AuthToken(token_data) = &self.auth {
349            match &token_data.auth_info {
350                Some(auth_data) => {
351                    if let Some(project) = &auth_data.token.project {
352                        self.catalog.set_project_id(project.id.clone());
353                        // Reconfigure catalog since we know now the project_id
354                        self.catalog.configure(&self.config)?;
355                    }
356                    if let Some(endpoints) = &auth_data.token.catalog {
357                        self.catalog
358                            .process_catalog_endpoints(endpoints, Some("public"))?;
359                    } else {
360                        error!("No catalog information");
361                    }
362                }
363                _ => return Err(OpenStackError::NoAuth),
364            }
365        }
366        // TODO: without AuthToken authorization we may want to read catalog separately
367        Ok(())
368    }
369
370    #[instrument(skip(self))]
371    pub fn discover_service_endpoint(
372        &mut self,
373        service_type: &ServiceType,
374    ) -> Result<(), OpenStackError> {
375        if let Ok(ep) = self.catalog.get_service_endpoint(
376            service_type.to_string(),
377            None,
378            self.config.region_name.as_ref(),
379        ) {
380            if self.catalog.discovery_allowed(service_type.to_string()) {
381                info!("Performing `{}` endpoint version discovery", service_type);
382
383                let orig_url = ep.url().clone();
384                let mut try_url = ep.url().clone();
385                let mut max_depth = 10;
386                loop {
387                    let req = http::Request::builder()
388                        .method(http::Method::GET)
389                        .uri(query::url_to_http_uri(try_url.clone()));
390
391                    let rsp = self.rest_with_auth(req, Vec::new(), &self.auth)?;
392                    if rsp.status() != StatusCode::NOT_FOUND
393                        && self
394                            .catalog
395                            .process_endpoint_discovery(
396                                service_type,
397                                &try_url,
398                                rsp.body(),
399                                None::<String>,
400                            )
401                            .is_ok()
402                    {
403                        debug!("Finished service version discovery at {}", try_url.as_str());
404                        return Ok(());
405                    }
406                    if try_url.path() != "/" {
407                        // We are not at the root yet and have not found a
408                        // valid version document so far, try one level up
409                        try_url = try_url.join("../")?;
410                    } else {
411                        return Err(OpenStackError::Discovery {
412                            service: service_type.to_string(),
413                            url: orig_url.into(),
414                            msg: match service_type {
415                                ServiceType::Identity => "Service is not working.".into(),
416                                _ => "No Version document found. Either service is not supporting version discovery, or API is not working".into(),
417                            }
418                        });
419                    }
420
421                    max_depth -= 1;
422                    if max_depth == 0 {
423                        break;
424                    }
425                }
426                return Err(OpenStackError::Discovery {
427                    service: service_type.to_string(),
428                    url: orig_url.into(),
429                    msg: "Unknown".into(),
430                });
431            }
432            return Ok(());
433        }
434        Ok(())
435    }
436
437    /// Return current authentication token
438    pub fn get_auth_token(&self) -> Option<String> {
439        if let Auth::AuthToken(token) = &self.auth {
440            return Some(token.token.clone());
441        }
442        None
443    }
444
445    /// Return current authentication status
446    ///
447    /// Offset can be used to calculate imminent expiration.
448    pub fn get_auth_state(&self, offset: Option<TimeDelta>) -> Option<AuthState> {
449        if let Auth::AuthToken(token) = &self.auth {
450            return Some(token.get_state(offset));
451        }
452        None
453    }
454
455    /// Perform HTTP request with given request and return raw response.
456    #[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(""))))]
457    fn execute_request(&self, request: Request) -> Result<Response, reqwest::Error> {
458        info!("Sending request {:?}", request);
459        let url: Url = request.url().clone();
460        let method = request.method().clone();
461
462        let start = SystemTime::now();
463        let rsp = self.client.execute(request)?;
464        let elapsed = SystemTime::now().duration_since(start).unwrap_or_default();
465        event!(
466            name: "http_request",
467            Level::INFO,
468            url=url.as_str(),
469            duration_ms=elapsed.as_millis(),
470            status=rsp.status().as_u16(),
471            method=method.as_str(),
472            request_id=rsp.headers().get("x-openstack-request-id").map(|v| v.to_str().unwrap_or("")),
473            "Request completed with status {}",
474            rsp.status(),
475        );
476        Ok(rsp)
477    }
478
479    /// Perform a REST query with a given auth.
480    fn rest_with_auth(
481        &self,
482        mut request: http::request::Builder,
483        body: Vec<u8>,
484        auth: &Auth,
485    ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
486        let call = || -> Result<_, RestError> {
487            auth.set_header(request.headers_mut().unwrap())?;
488            let http_request = request.body(body)?;
489            let request = http_request.try_into()?;
490
491            let rsp = self.execute_request(request)?;
492
493            let mut http_rsp = HttpResponse::builder()
494                .status(rsp.status())
495                .version(rsp.version());
496            let headers = http_rsp.headers_mut().unwrap();
497            for (key, value) in rsp.headers() {
498                headers.insert(key, value.clone());
499            }
500            Ok(http_rsp.body(rsp.bytes()?)?)
501        };
502        call().map_err(api::ApiError::client)
503    }
504}
505
506impl api::RestClient for OpenStack {
507    type Error = RestError;
508
509    /// Get service endpoint from the catalog
510    fn get_service_endpoint(
511        &self,
512        service_type: &ServiceType,
513        version: Option<&ApiVersion>,
514    ) -> Result<&ServiceEndpoint, api::ApiError<Self::Error>> {
515        Ok(self
516            .catalog
517            .get_service_endpoint(service_type.to_string(), version, None::<String>)?)
518    }
519
520    fn get_current_project(&self) -> Option<Project> {
521        if let Auth::AuthToken(token) = &self.auth {
522            return token.auth_info.clone().and_then(|x| x.token.project);
523        }
524        None
525    }
526}
527
528impl api::Client for OpenStack {
529    /// Perform the query with the client specifics
530    fn rest(
531        &self,
532        request: http::request::Builder,
533        body: Vec<u8>,
534    ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
535        self.rest_with_auth(request, body, &self.auth)
536    }
537}