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::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
74pub async fn entry_point() -> Result<(), OpenStackCliError> {
76 let cli = Cli::parse();
77
78 if let TopLevelCommands::Completion(args) = &cli.command {
79 let mut cmd = Cli::command();
81 cmd.set_bin_name(cmd.get_name().to_string());
82 cmd.build();
83 args.shell.try_generate(&cmd, &mut io::stdout()).ok();
85 return Ok(());
86 }
87
88 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 let request_stats = Arc::new(Mutex::new(HttpRequestStats::default()));
102 let rtl = RequestTracingCollector {
103 stats: request_stats.clone(),
104 }
105 .boxed();
106
107 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 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 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 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 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 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 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 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 (Some(project), _) => project.domain,
209 (None, Some(domain)) => Some(domain),
211 _ => 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 let res = cli.take_action(&mut session).await;
228
229 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
241fn 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}