openstack_cli/
lib.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#![doc = include_str!("../README.md")]
16#![deny(missing_docs)]
17// Allow Enum variant to end with enum's name
18// enum Type {
19//   ...
20//   FileType
21//   ...
22// }
23#![allow(clippy::enum_variant_names)]
24use std::io::{self, IsTerminal};
25
26use clap::{CommandFactory, Parser};
27use clap_complete::Generator;
28use dialoguer::FuzzySelect;
29use eyre::eyre;
30use std::sync::{Arc, Mutex};
31use tracing::warn;
32use tracing_subscriber::filter::LevelFilter;
33use tracing_subscriber::{Layer, prelude::*};
34
35use openstack_sdk::{
36    AsyncOpenStack,
37    auth::auth_helper::{Dialoguer, ExternalCmd, Noop},
38    auth::authtoken::AuthTokenScope,
39    types::identity::v3::Project,
40};
41
42pub mod api;
43pub mod auth;
44pub mod block_storage;
45pub mod catalog;
46mod common;
47pub mod compute;
48pub mod config;
49pub mod container_infrastructure_management;
50pub mod dns;
51pub mod identity;
52pub mod image;
53pub mod load_balancer;
54pub mod network;
55pub mod object_store;
56pub mod placement;
57
58mod tracing_stats;
59
60pub mod cli;
61pub mod error;
62pub mod output;
63
64use crate::error::OpenStackCliError;
65use crate::tracing_stats::{HttpRequestStats, RequestTracingCollector};
66
67pub use cli::Cli;
68use cli::TopLevelCommands;
69
70use comfy_table::ContentArrangement;
71use comfy_table::Table;
72use comfy_table::presets::UTF8_FULL_CONDENSED;
73
74/// Entry point for the CLI wrapper
75pub async fn entry_point() -> Result<(), OpenStackCliError> {
76    let cli = Cli::parse();
77
78    if let TopLevelCommands::Completion(args) = &cli.command {
79        // generate completion output
80        let mut cmd = Cli::command();
81        cmd.set_bin_name(cmd.get_name().to_string());
82        cmd.build();
83        // Ignore any error during writing the completion
84        args.shell.try_generate(&cmd, &mut io::stdout()).ok();
85        return Ok(());
86    }
87
88    // Initialize tracing layers
89    // fmt for console logging
90    let log_layer = tracing_subscriber::fmt::layer()
91        .with_writer(io::stderr)
92        .with_filter(match cli.global_opts.output.verbose {
93            0 => LevelFilter::WARN,
94            1 => LevelFilter::INFO,
95            2 => LevelFilter::DEBUG,
96            _ => LevelFilter::TRACE,
97        })
98        .boxed();
99
100    // RequestTracingCollector for capturing http statistics
101    let request_stats = Arc::new(Mutex::new(HttpRequestStats::default()));
102    let rtl = RequestTracingCollector {
103        stats: request_stats.clone(),
104    }
105    .boxed();
106
107    // build the tracing registry
108    tracing_subscriber::registry()
109        .with(log_layer)
110        .with(rtl)
111        .init();
112
113    let cloud_config = if cli.global_opts.connection.cloud_config_from_env {
114        // Environment variables should be used to get the cloud configuration
115        tracing::debug!("Using environment variables for the cloud connection");
116        let cloud_name = cli
117            .global_opts
118            .connection
119            .os_cloud_name
120            .clone()
121            .unwrap_or(String::from("envvars"));
122        let mut cloud_config = openstack_sdk::config::CloudConfig::from_env()?;
123        cloud_config.name = Some(cloud_name.clone());
124        cloud_config
125    } else {
126        // prepare cloud config parsing
127        let cfg = openstack_sdk::config::ConfigFile::new_with_user_specified_configs(
128            cli.global_opts.connection.os_client_config_file.as_deref(),
129            cli.global_opts.connection.os_client_secure_file.as_deref(),
130        )?;
131
132        // Identify target cloud to connect to
133        let cloud_name = match cli.global_opts.connection.os_cloud {
134            Some(ref cloud) => cloud.clone(),
135            None => {
136                if std::io::stdin().is_terminal() {
137                    // Cloud was not selected and we are in the potentially interactive mode (terminal)
138                    let mut profiles = cfg.get_available_clouds();
139                    profiles.sort();
140                    let selected_cloud_idx = FuzzySelect::new()
141                    .with_prompt("Please select cloud you want to connect to (use `--os-cloud` next time for efficiency)?")
142                    .items(&profiles)
143                    .interact()?;
144                    profiles[selected_cloud_idx].clone()
145                } else {
146                    return Err(
147                    eyre!("`--os-cloud` or `OS_CLOUD` environment variable must be given, or at least `--cloud-config-from-env` should be used.").into(),
148                );
149                }
150            }
151        };
152        cfg.get_cloud_config(&cloud_name)?
153            .ok_or(OpenStackCliError::ConnectionNotFound(cloud_name.clone()))?
154    };
155    let mut renew_auth: bool = false;
156
157    // Login command need to be analyzed before authorization
158    if let TopLevelCommands::Auth(args) = &cli.command {
159        if let auth::AuthCommands::Login(login_args) = &args.command {
160            if login_args.renew {
161                renew_auth = true;
162            }
163        }
164    }
165
166    // Connect to the selected cloud with the possible AuthHelper
167    let mut session =
168        if let Some(external_auth_helper) = &cli.global_opts.connection.auth_helper_cmd {
169            AsyncOpenStack::new_with_authentication_helper(
170                &cloud_config,
171                &mut ExternalCmd::new(external_auth_helper.clone()),
172                renew_auth,
173            )
174            .await
175        } else if std::io::stdin().is_terminal() {
176            AsyncOpenStack::new_with_authentication_helper(
177                &cloud_config,
178                &mut Dialoguer::default(),
179                renew_auth,
180            )
181            .await
182        } else {
183            AsyncOpenStack::new_with_authentication_helper(
184                &cloud_config,
185                &mut Noop::default(),
186                renew_auth,
187            )
188            .await
189        }
190        .map_err(|err| OpenStackCliError::Auth { source: err })?;
191
192    // Does the user want to connect to different project?
193    if cli.global_opts.connection.os_project_id.is_some()
194        || cli.global_opts.connection.os_project_name.is_some()
195    {
196        warn!(
197            "Cloud config is being chosen with arguments overriding project. Result may be not as expected."
198        );
199        let current_auth = session
200            .get_auth_info()
201            .ok_or(OpenStackCliError::MissingValidAuthenticationForRescope)?
202            .token;
203        let project = Project {
204            id: cli.global_opts.connection.os_project_id.clone(),
205            name: cli.global_opts.connection.os_project_name.clone(),
206            domain: match (current_auth.project, current_auth.domain) {
207                // New project is in the same domain as the original
208                (Some(project), _) => project.domain,
209                // domain scope was used
210                (None, Some(domain)) => Some(domain),
211                // There was no scope thus using user domain
212                _ => current_auth.user.domain,
213            },
214        };
215        let scope = AuthTokenScope::Project(project.clone());
216        session
217            .authorize(
218                Some(scope.clone()),
219                std::io::stdin().is_terminal(),
220                renew_auth,
221            )
222            .await
223            .map_err(|err| OpenStackCliError::ReScope { scope, source: err })?;
224    }
225
226    // Invoke the command
227    let res = cli.take_action(&mut session).await;
228
229    // If HTTP timing was requested dump stats into STDERR
230    if cli.global_opts.output.timing {
231        if let Ok(data) = request_stats.lock() {
232            let table = build_http_requests_timing_table(&data);
233            eprintln!("\nHTTP statistics:");
234            eprintln!("{table}");
235        }
236    }
237
238    res
239}
240
241/// Build a table of HTTP request timings
242fn build_http_requests_timing_table(data: &HttpRequestStats) -> Table {
243    let mut table = Table::new();
244    table
245        .load_preset(UTF8_FULL_CONDENSED)
246        .set_content_arrangement(ContentArrangement::Dynamic)
247        .set_header(Vec::from(["Url", "Method", "Duration (ms)"]));
248
249    let mut total_http_duration: u128 = 0;
250    for rec in data.summarize_by_url_method() {
251        total_http_duration += rec.2;
252        table.add_row(vec![rec.0, rec.1, rec.2.to_string()]);
253    }
254    table.add_row(vec!["Total", "", &total_http_duration.to_string()]);
255    table
256}