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::generate;
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, auth::authtoken::AuthTokenScope, types::identity::v3::Project,
37};
38
39pub mod api;
40pub mod auth;
41pub mod block_storage;
42pub mod catalog;
43mod common;
44pub mod compute;
45pub mod config;
46pub mod container_infrastructure_management;
47pub mod dns;
48pub mod identity;
49pub mod image;
50pub mod load_balancer;
51pub mod network;
52pub mod object_store;
53pub mod placement;
54
55mod tracing_stats;
56
57pub mod cli;
58pub mod error;
59pub mod output;
60
61use crate::error::OpenStackCliError;
62use crate::tracing_stats::{HttpRequestStats, RequestTracingCollector};
63
64pub use cli::Cli;
65use cli::TopLevelCommands;
66
67use comfy_table::ContentArrangement;
68use comfy_table::Table;
69use comfy_table::presets::UTF8_FULL_CONDENSED;
70
71/// Entry point for the CLI wrapper
72pub async fn entry_point() -> Result<(), OpenStackCliError> {
73    let cli = Cli::parse();
74
75    if let TopLevelCommands::Completion(args) = &cli.command {
76        // generate completion output
77        generate(
78            args.shell,
79            &mut Cli::command(),
80            Cli::command().get_name().to_string(),
81            &mut io::stdout(),
82        );
83        return Ok(());
84    }
85
86    // Initialize tracing layers
87    // fmt for console logging
88    let log_layer = tracing_subscriber::fmt::layer()
89        .with_writer(io::stderr)
90        .with_filter(match cli.global_opts.verbose {
91            0 => LevelFilter::WARN,
92            1 => LevelFilter::INFO,
93            2 => LevelFilter::DEBUG,
94            _ => LevelFilter::TRACE,
95        })
96        .boxed();
97
98    // RequestTracingCollector for capturing http statistics
99    let request_stats = Arc::new(Mutex::new(HttpRequestStats::default()));
100    let rtl = RequestTracingCollector {
101        stats: request_stats.clone(),
102    }
103    .boxed();
104
105    // build the tracing registry
106    tracing_subscriber::registry()
107        .with(log_layer)
108        .with(rtl)
109        .init();
110
111    // build configs
112    let cfg = openstack_sdk::config::ConfigFile::new_with_user_specified_configs(
113        cli.global_opts.os_client_config_file.as_deref(),
114        cli.global_opts.os_client_secure_file.as_deref(),
115    )?;
116
117    // Identify target cloud to connect to
118    let cloud_name = match cli.global_opts.os_cloud {
119        Some(ref cloud) => cloud.clone(),
120        None => {
121            if std::io::stdin().is_terminal() {
122                // Cloud was not selected and we are in the potentially interactive mode (terminal)
123                let mut profiles = cfg.get_available_clouds();
124                profiles.sort();
125                let selected_cloud_idx = FuzzySelect::new()
126                    .with_prompt("Please select cloud you want to connect to (use `--os-cloud` next time for efficiency)?")
127                    .items(&profiles)
128                    .interact()?;
129                profiles[selected_cloud_idx].clone()
130            } else {
131                return Err(
132                    eyre!("`--os-cloud` or `OS_CLOUD` environment variable must be given").into(),
133                );
134            }
135        }
136    };
137    // Get the connection details
138    let profile = cfg
139        .get_cloud_config(&cloud_name)?
140        .ok_or(OpenStackCliError::ConnectionNotFound(cloud_name))?;
141    let mut renew_auth: bool = false;
142
143    // Login command need to be analyzed before authorization
144    if let TopLevelCommands::Auth(args) = &cli.command {
145        if let auth::AuthCommands::Login(login_args) = &args.command {
146            if login_args.renew {
147                renew_auth = true;
148            }
149        }
150    }
151
152    let mut session;
153    if std::io::stdin().is_terminal() {
154        // Interactive session (may ask for password/MFA/SSO)
155        session = AsyncOpenStack::new_interactive(&profile, renew_auth)
156            .await
157            .map_err(|err| OpenStackCliError::Auth { source: err })?;
158    } else {
159        // Non-interactive session if i.e. scripted with chaining
160        session = AsyncOpenStack::new(&profile)
161            .await
162            .map_err(|err| OpenStackCliError::Auth { source: err })?;
163    }
164    // Does the user want to connect to different project?
165    if cli.global_opts.os_project_id.is_some() || cli.global_opts.os_project_name.is_some() {
166        warn!(
167            "Cloud config is being chosen with arguments overriding project. Result may be not as expected."
168        );
169        let current_auth = session
170            .get_auth_info()
171            .expect("Already authenticated")
172            .token;
173        let project = Project {
174            id: cli.global_opts.os_project_id.clone(),
175            name: cli.global_opts.os_project_name.clone(),
176            domain: match (current_auth.project, current_auth.domain) {
177                // New project is in the same domain as the original
178                (Some(project), _) => project.domain,
179                // domain scope was used
180                (None, Some(domain)) => Some(domain),
181                // There was no scope thus using user domain
182                _ => current_auth.user.domain,
183            },
184        };
185        let scope = AuthTokenScope::Project(project.clone());
186        session
187            .authorize(
188                Some(scope.clone()),
189                std::io::stdin().is_terminal(),
190                renew_auth,
191            )
192            .await
193            .map_err(|err| OpenStackCliError::ReScope { scope, source: err })?;
194    }
195
196    // Invoke the command
197    let res = cli.take_action(&mut session).await;
198
199    // If HTTP timing was requested dump stats into STDERR
200    if cli.global_opts.timing {
201        if let Ok(data) = request_stats.lock() {
202            let table = build_http_requests_timing_table(&data);
203            eprintln!("\nHTTP statistics:");
204            eprintln!("{table}");
205        }
206    }
207
208    res
209}
210
211/// Build a table of HTTP request timings
212fn build_http_requests_timing_table(data: &HttpRequestStats) -> Table {
213    let mut table = Table::new();
214    table
215        .load_preset(UTF8_FULL_CONDENSED)
216        .set_content_arrangement(ContentArrangement::Dynamic)
217        .set_header(Vec::from(["Url", "Method", "Duration (ms)"]));
218
219    let mut total_http_duration: u128 = 0;
220    for rec in data.summarize_by_url_method() {
221        total_http_duration += rec.2;
222        table.add_row(vec![rec.0, rec.1, rec.2.to_string()]);
223    }
224    table.add_row(vec!["Total", "", &total_http_duration.to_string()]);
225    table
226}