1use std::sync::Arc;
2
3use tokio::sync::RwLock;
4use super::super::types::structs;
5use log::{warn, debug, trace, log_enabled};
6use log::Level::Trace;
7
8use std::ffi::OsStr;
9use crate::mo;
10use crate::types::structs::{ManagedObjectReference, ServiceContent};
11
12const LIB_NAME: &str = env!("CARGO_PKG_NAME");
13const LIB_VERSION: &str = env!("CARGO_PKG_VERSION");
14const RUSTC_VERSION: &str = env!("RUSTC_VERSION");
16
17pub const COMPATIBLE_API_RELEASES: [&str; 2] = ["8.0.2.0", "8.0.1.0"];
19
20pub const API_RELEASE: &str = "8.0.2.0";
22
23const AUTHN_HEADER: &str = "vmware-api-session-id";
25
26const SERVICE_INSTANCE_MOID: &str = "ServiceInstance";
27
28#[derive(Debug, thiserror::Error)]
29pub enum Error {
30 #[error("MethodFault: {0:?}")]
31 MethodFault(structs::MethodFault),
32 #[error("Reqwest error: {0}")]
33 ReqwestError(#[from] reqwest::Error),
34 #[error("Serde error: {0}")]
35 SerdeError(#[from] serde_json::Error),
36 #[error("Missing or Invalid session key")]
37 MissingOrInvalidSessionKey,
38 #[error("Invalid object type {0} expected: {1}")]
39 InvalidObjectType(String, String),
40 #[error("Cannot negotiate compatible API release. Attempted with: {0:?}")]
41 CannotNegotiateAPIRelease(Vec<String>),
42}
43
44pub type Result<T> = std::result::Result<T, Error>;
45
46pub struct ClientBuilder {
47 server_address: String,
48 compatible_api_releases: Option<Vec<String>>,
49 api_release: Option<String>,
50 http_client: Option<reqwest::Client>,
51 insecure: Option<bool>,
52 app_name: Option<String>,
53 app_version: Option<String>,
54 user_name: Option<String>,
55 password: Option<String>,
56 locale: Option<String>,
57}
58
59impl ClientBuilder {
60 pub fn new(server_address: &str) -> Self {
64 Self {
65 server_address: server_address.to_string(),
66 compatible_api_releases: None,
67 api_release: None,
68 http_client: None,
69 insecure: None,
70 app_name: None,
71 app_version: None,
72 user_name: None,
73 password: None,
74 locale: None,
75 }
76 }
77
78 pub fn compatible_api_releases(mut self, releases: Vec<&str>) -> Self {
84 self.compatible_api_releases = Some(releases.iter().map(|s| s.to_string()).collect());
85 self
86 }
87
88 pub fn api_release(mut self, api_release: &str) -> Self {
93 self.api_release = Some(api_release.to_string());
94 self
95 }
96
97 pub fn http_client(mut self, http_client: reqwest::Client) -> Self {
102 self.http_client = Some(http_client);
103 self.insecure = None;
104 self
105 }
106
107 pub fn insecure(mut self, insecure: bool) -> Self {
111 warn!("!!! WARNING !!! Insecure mode enabled. TLS certificate and hostname verification is disabled. !!! WARNING !!!");
112 self.insecure = Some(insecure);
113 self.http_client = None;
114 self
115 }
116
117 pub fn app_details(mut self, app_name: &str, app_version: &str) -> Self {
127 self.app_name = Some(app_name.to_string());
128 self.app_version = Some(app_version.to_string());
129 self
130 }
131
132 pub fn basic_authn(mut self, user_name: &str, password: &str) -> Self {
136 self.user_name = Some(user_name.to_string());
137 self.password = Some(password.to_string());
138 self
139 }
140
141 pub fn locale(mut self, locale: &str) -> Self {
144 self.locale = Some(locale.to_string());
145 self
146 }
147
148 pub async fn build(self) -> Result<Arc<Client>> {
150 let http_client = match self.http_client {
151 Some(client) => client,
152 None => {
153 let mut builder = reqwest::ClientBuilder::new();
154 if let Some(insecure) = self.insecure {
155 builder = builder.danger_accept_invalid_certs(insecure)
156 .danger_accept_invalid_hostnames(insecure);
157 }
158 builder.build()?
159 },
160 };
161 let session_key = Arc::new(RwLock::new(None));
162
163 let user_agent = user_agent(self.app_name.as_deref(), self.app_version.as_deref());
164
165 let api_release = match self.api_release {
167 Some(release) => release,
168 None => {
169 let releases = self.compatible_api_releases
170 .unwrap_or_else(|| COMPATIBLE_API_RELEASES.iter().map(|s| s.to_string()).collect());
171 let spec = HelloSpec {
172 api_releases: &releases,
173 };
174 let path = format!("https://{}/api/vcenter/system?action=hello", self.server_address);
175 let req = http_client.post(&path)
176 .header("Content-Type", "application/json")
177 .header("User-Agent", &user_agent)
178 .json(&spec);
179 let res = req.send().await?;
180 let res = res.error_for_status()?;
181 let result: HelloResult = res.json().await?;
182 let api_release = result.api_release;
183 if api_release.is_empty() {
186 return Err(Error::CannotNegotiateAPIRelease(releases));
187 }
188 debug!("Negotiated API release: {}", api_release);
189 api_release
190 },
191 };
192
193 let base_url = format!("https://{}/sdk/vim25/{}", self.server_address, api_release);
194
195 let bootstrap = Arc::new(Client {
196 http_client: http_client.clone(),
197 session_key: session_key.clone(),
198 api_release: api_release.clone(),
199 base_url: base_url.clone(),
200 user_agent: user_agent.clone(),
201 service_content: None,
202 });
203
204 let service_instance = mo::ServiceInstance::new(bootstrap.clone(), SERVICE_INSTANCE_MOID);
205 let content = service_instance.content().await?;
206 debug!("ServiceInstance content obtained from: {}", content.about.full_name);
207 trace!("ServiceInstance content: {:?}", content);
208
209 let sm_id = content.session_manager.as_ref().map(|moid| moid.value.clone());
210 let client = Arc::new(Client {
211 http_client: http_client.clone(),
212 session_key: session_key.clone(),
213 api_release: api_release.clone(),
214 base_url: base_url.clone(),
215 user_agent: user_agent.clone(),
216 service_content: Some(content),
217 });
218
219
220 if let (Some(ref sm_id), Some(ref user_name), Some(ref password)) = (sm_id, self.user_name, self.password) {
221 let sm = mo::SessionManager::new(client.clone(), sm_id);
222 let session = sm.login(user_name, password, self.locale.as_deref()).await?;
223 debug!("Session created for: {:?}", session.user_name);
224 }
225 Ok(client)
226 }
227}
228
229pub struct Client {
230 http_client: reqwest::Client,
231 session_key: Arc<RwLock<Option<String>>>,
232 api_release: String,
233 base_url: String,
234 user_agent: String,
235 service_content: Option<ServiceContent>,
236}
237
238impl Client {
243
244 pub fn service_content(&self) -> &ServiceContent {
246 self.service_content.as_ref().unwrap()
248 }
249
250 pub fn api_release(&self) -> String {
255 self.api_release.clone()
256 }
257
258 pub async fn fetch_property<T>(&self, obj: ManagedObjectReference, property: &str) -> Result<T>
262 where
263 T: serde::de::DeserializeOwned
264 {
265 let type_name: &str = obj.r#type.into();
266 let id = &obj.value;
267 let path = format!("/{type_name}/{id}/{property}");
268 let req = self.get_request(&path);
269 self.execute(req).await
270 }
271
272 pub(crate) fn get_request(&self, path: &str) -> reqwest::RequestBuilder
274 {
275 debug!("GET request: {}", path);
276 let url = format!("{}{}", self.base_url, path);
277 self.http_client.get(&url)
278 }
279
280 pub(crate) fn post_request<B>(&self, path: &str, payload: &B) -> reqwest::RequestBuilder
282 where
283 B: serde::Serialize,
284 {
285 debug!("POST request: {}", path);
286 let url = format!("{}{}", self.base_url, path);
287 let req = self.http_client.post(&url);
288 req.header("Content-Type", "application/json").json(payload)
289 }
290
291 pub(crate) fn post_bare(&self, path: &str) -> reqwest::RequestBuilder
293 {
294 debug!("POST request (void): {}", path);
295 let url = format!("{}{}", self.base_url, path);
296 self.http_client.post(&url)
297 }
298
299 pub(crate) async fn execute<T>(&self, mut req: reqwest::RequestBuilder) -> Result<T>
301 where T: serde::de::DeserializeOwned
302 {
303 req = self.prepare(req).await;
304 let res = req.send().await?;
305 let res = self.process_response(res).await?;
306 let content: T = res.json().await?;
307 Ok(content)
308 }
309
310 pub(crate) async fn execute_option<T>(&self, mut req: reqwest::RequestBuilder) -> Result<Option<T>>
312 where T: serde::de::DeserializeOwned
313 {
314 req = self.prepare(req).await;
315 let res = req.send().await?;
316 let res = self.process_response(res).await?;
317 let bytes = res.bytes().await?;
318 if log_enabled!(Trace) {
319 trace!("Response body: {}", std::str::from_utf8(&bytes).unwrap());
320 }
321 let r: serde_json::Result<T> = serde_json::from_slice(&bytes);
322 let content = match r {
323 Ok(c) => Some(c),
324 Err(e) => {
325 if e.is_eof() {
326 None
327 } else {
328 return Err(Error::SerdeError(e));
329 }
330 },
331 };
332 Ok(content)
333 }
334
335 pub(crate) async fn execute_void(&self, mut req: reqwest::RequestBuilder) -> Result<()>
337 {
338 req = self.prepare(req).await;
339 let res = req.send().await?;
340 let _ = self.process_response(res).await?;
341 Ok(())
342 }
343
344 async fn prepare(&self, mut req: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
346 let session_key = self.session_key.read().await;
347 if let Some(value) = session_key.as_ref() {
348 req = req.header(AUTHN_HEADER, value);
349 }
350 req = req.header("User-Agent", &self.user_agent);
351 req
352 }
353
354 async fn process_response(&self, res: reqwest::Response) -> Result<reqwest::Response> {
356 if res.status().is_success() && res.headers().contains_key(AUTHN_HEADER) {
357 let session_key = res.headers().get(AUTHN_HEADER).unwrap().to_str().map_err(|_| Error::MissingOrInvalidSessionKey)?.to_string();
358 let mut key_holder = self.session_key.write().await;
359 *key_holder = Some(session_key);
360 }
361 if !res.status().is_success() {
362 warn!("HTTP error: {}", res.status());
363 let fault: structs::MethodFault = res.json().await?;
364 return Err(Error::MethodFault(fault));
365 }
366 Ok(res)
367 }
368}
369
370
371impl Drop for Client {
374 fn drop(&mut self) {
375 debug!("Disposing VIM client.");
376
377 let session_key = Arc::clone(&self.session_key);
378 let http_client = &self.http_client.clone();
379 let base_url = self.base_url.clone();
380
381 let sm_id = self.service_content.as_ref().and_then(|content| content.session_manager.as_ref().map(|moid| moid.value.clone()));
382 let sm_id = match sm_id {
383 Some(id) => id,
384 None => {
385 debug!("No session manager found. Skipping logout.");
386 return;
387 },
388 };
389
390 tokio::task::block_in_place(|| {
391 tokio::runtime::Handle::current().block_on(async move {
392 debug!("Terminating VIM session as needed.");
393 let key = {
394 let session_key = session_key.read().await;
395 session_key.clone()
396 };
397 let Some(key) = key else {
398 debug!("No session key present. Skipping logout.");
399 return;
400 };
401 debug!("Session is present. Sending logout request...");
402
403 let path = format!("{base_url}/SessionManager/{moId}/Logout",
404 base_url = base_url,
405 moId = sm_id);
406 let req = http_client.post(&path)
407 .header(AUTHN_HEADER, key);
408 match req.send().await {
409 Ok(resp) => {
410 let status = resp.status();
411 if status.is_success() {
412 debug!("Session logged out successfully");
413 } else {
414 resp.json::<structs::MethodFault>().await.map(|fault| {
415 warn!("Failed to logout session(HTTP code: {}). MethodFault: {:?}", status, fault);
416 }).unwrap_or_else(|e| {
417 warn!("Failed to logout session(HTTP code: {}). Cannot parse MethodFault: {}", status, e);
418 });
419 }
420 },
421 Err(e) => warn!("Failed to logout session. Cannot execute logout request: {}", e),
422 }
423 });
424 });
425 }
426}
427
428fn user_agent(app_name: Option<&str>, app_version: Option<&str>) -> String {
429 let app_name: String = if app_name.is_some() {
430 app_name.unwrap().to_string()
431 } else {
432 get_executable_name().unwrap_or_else(|| "unknown".to_string())
433 };
434 let Some(appv) = app_version else {
435 return format!(
436 "{} ({}/{}; {}; {}; rustc/{})",
437 app_name,
438 LIB_NAME,
439 LIB_VERSION,
440 std::env::consts::OS,
441 std::env::consts::ARCH,
442 RUSTC_VERSION
443 );
444 };
445 format!(
446 "{}/{} ({}/{}; {}; {}; rustc/{})",
447 app_name,
448 appv,
449 LIB_NAME,
450 LIB_VERSION,
451 std::env::consts::OS,
452 std::env::consts::ARCH,
453 RUSTC_VERSION
454 )
455}
456
457fn get_executable_name() -> Option<String> {
458 std::env::current_exe()
459 .ok()
460 .as_ref()
461 .and_then(|path| path.file_name())
462 .and_then(OsStr::to_str)
463 .map(|s| s.to_owned())
464}
465
466
467#[derive(serde::Serialize, Debug)]
471struct HelloSpec<'a> {
472 api_releases: &'a Vec<String>,
474}
475
476#[derive(serde::Deserialize, Debug)]
479struct HelloResult {
480 api_release: String,
485}