1#![doc = include_str!("../README.md")]
16#![deny(missing_docs)]
17#![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
71pub async fn entry_point() -> Result<(), OpenStackCliError> {
73 let cli = Cli::parse();
74
75 if let TopLevelCommands::Completion(args) = &cli.command {
76 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 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 let request_stats = Arc::new(Mutex::new(HttpRequestStats::default()));
100 let rtl = RequestTracingCollector {
101 stats: request_stats.clone(),
102 }
103 .boxed();
104
105 tracing_subscriber::registry()
107 .with(log_layer)
108 .with(rtl)
109 .init();
110
111 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 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 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 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 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 session = AsyncOpenStack::new_interactive(&profile, renew_auth)
156 .await
157 .map_err(|err| OpenStackCliError::Auth { source: err })?;
158 } else {
159 session = AsyncOpenStack::new(&profile)
161 .await
162 .map_err(|err| OpenStackCliError::Auth { source: err })?;
163 }
164 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 (Some(project), _) => project.domain,
179 (None, Some(domain)) => Some(domain),
181 _ => 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 let res = cli.take_action(&mut session).await;
198
199 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
211fn 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}