1use crate::impact_provider::{CloudResourceWithImpacts, ImpactProvider, ImpactsValues};
3use anyhow::Result;
4use boavizta_api_sdk::apis::cloud_api;
5use boavizta_api_sdk::apis::component_api;
6use boavizta_api_sdk::apis::configuration;
7use boavizta_api_sdk::apis::utils_api;
8use chrono::Utc;
9use std::time::{Duration, Instant};
10
11use crate::model::{
12 CloudResource, EstimatedInventory, EstimationMetadata, ExecutionStatistics, Inventory,
13 ResourceDetails,
14};
15use boavizta_api_sdk::models::{Cloud, Disk, UsageCloud};
16
17pub struct BoaviztaApiV1 {
19 configuration: boavizta_api_sdk::apis::configuration::Configuration,
20}
21
22impl BoaviztaApiV1 {
24 pub fn new(api_url: &str) -> Self {
25 let mut configuration = configuration::Configuration::new();
26 configuration.base_path = api_url.to_string();
27 BoaviztaApiV1 { configuration }
28 }
29
30 async fn get_api_version(&self) -> Option<String> {
32 let res = utils_api::version_v1_utils_version_get(&self.configuration).await;
33 if let Ok(serde_json::Value::String(v)) = res {
34 Some(v)
35 } else {
36 error!("Cannot fetch API version");
37 None
38 }
39 }
40
41 async fn get_raws_impacts(
43 &self,
44 cr: CloudResource,
45 usage_duration_hours: &f32,
46 verbose: bool,
47 ) -> Option<serde_json::Value> {
48 let resource_details = cr.resource_details;
49 let criteria = vec!["gwp".to_owned(), "adp".to_owned(), "pe".to_owned()];
50
51 match resource_details {
52 ResourceDetails::Instance {
53 instance_type,
54 usage,
55 } => {
56 let mut usage_cloud: UsageCloud = UsageCloud::new();
57
58 usage_cloud.usage_location = Some(Some(cr.location.iso_country_code.to_owned()));
60
61 if let Some(instance_usage) = usage {
62 usage_cloud.time_workload = Some(instance_usage.average_cpu_load);
63 }
64
65 let mut cloud: Cloud = Cloud::new();
66 cloud.provider = Some(Some(String::from("aws")));
67 cloud.instance_type = Some(Some(instance_type.clone()));
68 cloud.usage = Some(Some(Box::new(usage_cloud)));
69
70 let res = cloud_api::instance_cloud_impact_v1_cloud_instance_post(
71 &self.configuration,
72 Some(verbose),
73 Some(usage_duration_hours.to_owned().into()),
74 Some(criteria),
75 Some(cloud),
76 )
77 .await;
78
79 match res {
80 Ok(res) => Some(res),
81 Err(e) => {
82 warn!(
83 "Warning: Cannot get impacts from API for instance type {}: {}",
84 instance_type, e
85 );
86 None
87 }
88 }
89 }
90
91 ResourceDetails::BlockStorage {
92 storage_type,
93 usage,
94 attached_instances: _,
95 } => {
96 let disk = Disk {
98 capacity: Some(Some(usage.unwrap().size_gb)),
99 units: None,
100 usage: None,
101 r#type: None,
102 density: None,
103 manufacturer: None,
104 model: None,
105 layers: None,
106 };
107
108 match storage_type.as_str() {
109 "st1" | "sc1" | "standard" => {
110 let res = component_api::disk_impact_bottom_up_v1_component_hdd_post(
112 &self.configuration,
113 Some(verbose),
114 Some(usage_duration_hours.to_owned().into()),
115 Some("DEFAULT"),
116 Some(criteria),
117 Some(disk),
118 )
119 .await;
120 match res {
121 Ok(res) => Some(res),
122 Err(e) => {
123 warn!(
124 "Warning: Cannot get HHD impact from API for storage type {}: {}",
125 storage_type, e
126 );
127 None
128 }
129 }
130 }
131 "gp2" | "gp3" | "io1" | "io2" => {
132 let res = component_api::disk_impact_bottom_up_v1_component_ssd_post(
134 &self.configuration,
135 Some(verbose),
136 Some(usage_duration_hours.to_owned().into()),
137 Some("DEFAULT"),
138 Some(criteria),
139 Some(disk),
140 )
141 .await;
142 match res {
143 Ok(res) => Some(res),
144 Err(e) => {
145 warn!(
146 "Warning: Cannot get SSD impact from API for storage type {}: {}",
147 storage_type, e
148 );
149 None
150 }
151 }
152 }
153 _ => {
154 warn!(
155 "Unknown storage type ({:?}), defaulting to using impacts of an SSD {:?}",
156 storage_type.as_str(),
157 disk
158 );
159 let res = component_api::disk_impact_bottom_up_v1_component_ssd_post(
161 &self.configuration,
162 Some(verbose),
163 Some(usage_duration_hours.to_owned().into()),
164 Some("DEFAULT"),
165 Some(criteria),
166 Some(disk),
167 )
168 .await;
169 match res {
170 Ok(res) => Some(res),
171 Err(e) => {
172 warn!(
173 "Warning: Cannot get SSD impact from API for type {}: {}",
174 storage_type, e
175 );
176 None
177 }
178 }
179 }
180 }
181 }
182 _ => {
183 warn!("Warning: This type of cloud resource is not supported.");
184 None
185 }
186 }
187 }
188
189 async fn get_resource_with_impacts(
191 &self,
192 resource: &CloudResource,
193 usage_duration_hours: &f32,
194 verbose: bool,
195 ) -> CloudResourceWithImpacts {
196 let raw_impacts = self
197 .get_raws_impacts(resource.clone(), usage_duration_hours, verbose)
198 .await;
199 boa_impacts_to_cloud_resource_with_impacts(resource, &raw_impacts, usage_duration_hours)
200 }
201}
202
203#[async_trait]
204impl ImpactProvider for BoaviztaApiV1 {
205 async fn get_impacts(
208 &self,
209 inventory: Inventory,
210 usage_duration_hours: &f32,
211 verbose: bool,
212 ) -> Result<EstimatedInventory> {
213 let impact_query_start_time = Instant::now();
214
215 let mut v: Vec<CloudResourceWithImpacts> = Vec::new();
216 for resource in inventory.resources.iter() {
217 let cri = self
218 .get_resource_with_impacts(resource, usage_duration_hours, verbose)
219 .await;
220 v.push(cri.clone());
221 }
222
223 let mut inventory_duration = Duration::from_millis(0);
224 if let Some(exec_stats) = inventory.metadata.execution_statistics {
225 inventory_duration = exec_stats.inventory_duration;
226 }
227 let impact_estimation_duration = impact_query_start_time.elapsed();
228 let execution_statistics = ExecutionStatistics {
229 inventory_duration,
230 impact_estimation_duration,
231 total_duration: inventory_duration + impact_estimation_duration,
232 };
233
234 let estimated_inventory: EstimatedInventory = EstimatedInventory {
235 impacting_resources: v,
236 metadata: EstimationMetadata {
237 estimation_date: Some(Utc::now()),
238 description: Some("Estimation using Boavizta API".to_string()),
239 cloud_scanner_version: Some(crate::get_version()),
240 boavizta_api_version: self.get_api_version().await,
241 execution_statistics: Some(execution_statistics),
242 },
243 };
244 Ok(estimated_inventory)
245 }
246}
247
248pub fn boa_impacts_to_cloud_resource_with_impacts(
250 cloud_resource: &CloudResource,
251 raw_result: &Option<serde_json::Value>,
252 impacts_duration_hours: &f32,
253) -> CloudResourceWithImpacts {
254 let resource_impacts: Option<ImpactsValues>;
255 if let Some(results) = raw_result {
256 debug!("Raw results before conversion: {}", results);
257
258 let impacts = &results["impacts"];
259
260 let resource_details = cloud_resource.resource_details.clone();
261
262 match resource_details {
263 ResourceDetails::Instance {
264 instance_type: _,
265 usage: _,
266 } => {
267 resource_impacts = Some(ImpactsValues {
268 adp_manufacture_kgsbeq: impacts["adp"]["embedded"]["value"].as_f64().unwrap(),
269 adp_use_kgsbeq: impacts["adp"]["use"]["value"].as_f64().unwrap(),
270 pe_manufacture_megajoules: impacts["pe"]["embedded"]["value"].as_f64().unwrap(),
271 pe_use_megajoules: impacts["pe"]["use"]["value"].as_f64().unwrap(),
272 gwp_manufacture_kgco2eq: impacts["gwp"]["embedded"]["value"].as_f64().unwrap(),
273 gwp_use_kgco2eq: impacts["gwp"]["use"]["value"].as_f64().unwrap(),
274 raw_data: raw_result.clone(),
275 });
276 }
277 ResourceDetails::BlockStorage {
278 storage_type: _,
279 usage: _,
280 attached_instances: _,
281 } => {
282 info!("Impacts of the use phase of storage are not counted (only embedded impacts are counted).");
284 resource_impacts = Some(ImpactsValues {
285 adp_manufacture_kgsbeq: impacts["adp"]["embedded"]["value"].as_f64().unwrap(),
286 adp_use_kgsbeq: 0 as f64,
287 pe_manufacture_megajoules: impacts["pe"]["embedded"]["value"].as_f64().unwrap(),
288 pe_use_megajoules: 0 as f64,
289 gwp_manufacture_kgco2eq: impacts["gwp"]["embedded"]["value"].as_f64().unwrap(),
290 gwp_use_kgco2eq: 0 as f64,
291 raw_data: raw_result.clone(),
292 });
293 }
294 _ => {
295 resource_impacts = None;
296 }
297 }
298 } else {
299 debug!(
300 "Skipped resource: {:#?} while converting impacts, it has no impact data",
301 cloud_resource
302 );
303 resource_impacts = None;
304 };
305 CloudResourceWithImpacts {
306 cloud_resource: cloud_resource.clone(),
307 impacts_values: resource_impacts,
308 impacts_duration_hours: impacts_duration_hours.to_owned(),
309 }
310}
311
312#[cfg(test)]
313mod tests {
314
315 use super::*;
316 use crate::get_version;
317 use crate::model::{
318 CloudProvider, CloudResource, InstanceState, InstanceUsage, InventoryMetadata,
319 ResourceDetails, StorageUsage,
320 };
321 use crate::usage_location::UsageLocation;
322 use assert_json_diff::assert_json_include;
323 use assert_json_diff::{assert_json_matches, CompareMode, Config, NumericMode};
324
325 const TEST_API_URL: &str = "https://api.boavizta.org";
326 const DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR: &str =
332 include_str!("../test-data/DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR.json");
333 const DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR_VERBOSE: &str =
334 include_str!("../test-data/DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR_VERBOSE.json");
335
336 const DEFAULT_RAW_IMPACTS_OF_HDD: &str =
337 include_str!("../test-data/DEFAULT_RAW_IMPACTS_OF_HDD.json");
338
339 const DEFAULT_RAW_IMPACTS_OF_SSD_1000GB_1HR: &str =
340 include_str!("../test-data/DEFAULT_RAW_IMPACTS_OF_SSD_1000GB_1HR.json");
341
342 #[tokio::test]
343 async fn retrieve_instance_types_through_sdk_works() {
344 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
345 let provider = Some("aws");
346
347 let res = cloud_api::server_get_all_archetype_name_v1_cloud_instance_all_instances_get(
348 &api.configuration,
349 provider,
350 )
351 .await
352 .unwrap();
353 println!("{:?}", res);
354 }
355
356 #[tokio::test]
357 async fn get_api_version() {
358 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
359 let version = api.get_api_version().await;
360 let expected = Some("1.3.7".to_owned());
361 assert_eq!(version, expected, "Versions do not match");
362 }
363
364 #[tokio::test]
365 async fn should_retrieve_raw_default_impacts_aws_fr() {
366 let instance1: CloudResource = CloudResource {
367 provider: CloudProvider::AWS,
368 id: "inst-1".to_string(),
369 location: UsageLocation::try_from("eu-west-3").unwrap(),
370 resource_details: ResourceDetails::Instance {
371 instance_type: "m6g.xlarge".to_string(),
372 usage: Some(InstanceUsage {
373 average_cpu_load: 100.0,
374 state: InstanceState::Running,
375 }),
376 },
377 tags: Vec::new(),
378 };
379 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
380 let one_hour = 1.0 as f32;
381 let res = api
382 .get_raws_impacts(instance1, &one_hour, false)
383 .await
384 .unwrap();
385
386 let expected: serde_json::Value =
387 serde_json::from_str(DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR).unwrap();
388 assert_json_include!(actual: res, expected: expected);
389 }
390
391 #[tokio::test]
392 async fn get_verbose_raw_impacts_of_a_hdd() {
393 let hdd: CloudResource = CloudResource {
394 provider: CloudProvider::AWS,
395 id: "disk-1".to_string(),
396
397 location: UsageLocation::try_from("eu-west-3").unwrap(),
398 resource_details: ResourceDetails::BlockStorage {
399 storage_type: "st1".to_string(),
400 usage: Some(StorageUsage { size_gb: 1000 }),
401 attached_instances: None,
402 },
403 tags: Vec::new(),
404 };
405
406 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
407 let one_hour = 1.0 as f32;
408 let res = api.get_raws_impacts(hdd, &one_hour, true).await.unwrap();
409
410 let expected: serde_json::Value = serde_json::from_str(DEFAULT_RAW_IMPACTS_OF_HDD).unwrap();
411
412 let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat);
413 assert_json_matches!(res, expected, config);
414 }
415
416 #[tokio::test]
418 async fn get_verbose_raw_impacts_of_a_ssd() {
419 let ssd: CloudResource = CloudResource {
420 provider: CloudProvider::AWS,
421 id: "disk-1".to_string(),
422 location: UsageLocation::try_from("eu-west-3").unwrap(),
423 resource_details: ResourceDetails::BlockStorage {
424 storage_type: "gp2".to_string(),
425 usage: Some(StorageUsage { size_gb: 1000 }),
426 attached_instances: None,
427 },
428 tags: Vec::new(),
429 };
430
431 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
432 let one_hour = 1.0 as f32;
433 let res = api.get_raws_impacts(ssd, &one_hour, true).await.unwrap();
434
435 let expected: serde_json::Value =
436 serde_json::from_str(DEFAULT_RAW_IMPACTS_OF_SSD_1000GB_1HR).unwrap();
437
438 let config = Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat);
439 assert_json_matches!(res, expected, config);
440 }
441
442 #[tokio::test]
443 async fn returns_different_pe_impacts_for_different_cpu_load() {
444 let instance1: CloudResource = CloudResource {
445 provider: CloudProvider::AWS,
446 id: "inst-1".to_string(),
447 location: UsageLocation::try_from("eu-west-3").unwrap(),
448 resource_details: ResourceDetails::Instance {
449 instance_type: "m6g.xlarge".to_string(),
450 usage: Some(InstanceUsage {
451 average_cpu_load: 100.0,
452 state: InstanceState::Running,
453 }),
454 },
455 tags: Vec::new(),
456 };
457
458 let instance1_1percent = CloudResource {
459 provider: CloudProvider::AWS,
460 id: "inst-2".to_string(),
461 location: UsageLocation::try_from("eu-west-3").unwrap(),
462 resource_details: ResourceDetails::Instance {
463 instance_type: "m6g.xlarge".to_string(),
464 usage: Some(InstanceUsage {
465 average_cpu_load: 1.0,
466 state: InstanceState::Running,
467 }),
468 },
469 tags: Vec::new(),
470 };
471
472 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
473 let one_hour = 1.0 as f32;
474
475 let mut instances: Vec<CloudResource> = Vec::new();
476 instances.push(instance1);
477 instances.push(instance1_1percent);
478
479 let inventory = Inventory {
480 metadata: InventoryMetadata {
481 inventory_date: None,
482 description: None,
483 cloud_scanner_version: Some(get_version()),
484 execution_statistics: None,
485 },
486 resources: instances,
487 };
488
489 let res = api.get_impacts(inventory, &one_hour, false).await.unwrap();
490
491 let r0 = res.impacting_resources[0].impacts_values.clone().unwrap();
492 let r1 = res.impacting_resources[1].impacts_values.clone().unwrap();
493 assert_eq!(0.212, r0.pe_use_megajoules);
494 assert_eq!(0.088, r1.pe_use_megajoules);
495 }
496
497 #[tokio::test]
498 async fn should_retrieve_multiple_default_impacts_fr() {
499 let instance1: CloudResource = CloudResource {
500 provider: CloudProvider::AWS,
501 id: "inst-1".to_string(),
502 location: UsageLocation::try_from("eu-west-3").unwrap(),
503 resource_details: ResourceDetails::Instance {
504 instance_type: "m6g.xlarge".to_string(),
505 usage: Some(InstanceUsage {
506 average_cpu_load: 100.0,
507 state: InstanceState::Running,
508 }),
509 },
510 tags: Vec::new(),
511 };
512
513 let instance2: CloudResource = CloudResource {
514 provider: CloudProvider::AWS,
515 id: "inst-2".to_string(),
516 location: UsageLocation::try_from("eu-west-3").unwrap(),
517 resource_details: ResourceDetails::Instance {
518 instance_type: "m6g.xlarge".to_string(),
519 usage: Some(InstanceUsage {
520 average_cpu_load: 100.0,
521 state: InstanceState::Running,
522 }),
523 },
524 tags: Vec::new(),
525 };
526
527 let instance3: CloudResource = CloudResource {
528 provider: CloudProvider::AWS,
529 id: "inst-3".to_string(),
530 location: UsageLocation::try_from("eu-west-3").unwrap(),
531 resource_details: ResourceDetails::Instance {
532 instance_type: "type-not-in-boa".to_string(),
533 usage: Some(InstanceUsage {
534 average_cpu_load: 100.0,
535 state: InstanceState::Running,
536 }),
537 },
538 tags: Vec::new(),
539 };
540
541 let mut instances: Vec<CloudResource> = Vec::new();
542 instances.push(instance1);
543 instances.push(instance2);
544 instances.push(instance3);
545 let one_hour = 1.0 as f32;
546
547 let inventory = Inventory {
548 metadata: InventoryMetadata {
549 inventory_date: None,
550 description: None,
551 cloud_scanner_version: Some(get_version()),
552 execution_statistics: None,
553 },
554 resources: instances,
555 };
556
557 let api: BoaviztaApiV1 = BoaviztaApiV1::new(TEST_API_URL);
558 let res = api.get_impacts(inventory, &one_hour, false).await.unwrap();
559
560 assert_eq!(3, res.impacting_resources.len());
561 assert_eq!(res.impacting_resources[0].cloud_resource.id, "inst-1");
562 assert_eq!(res.impacting_resources[1].cloud_resource.id, "inst-2");
563
564 let r0 = res.impacting_resources[0].impacts_values.clone().unwrap();
565 let r1 = res.impacting_resources[1].impacts_values.clone().unwrap();
566
567 assert_eq!(0.212, r0.pe_use_megajoules);
568 assert_eq!(0.212, r1.pe_use_megajoules);
569 assert!(
570 res.impacting_resources[2].impacts_values.clone().is_none(),
571 "This instance should return None impacts because it's type is unknown from API"
572 );
573 }
574
575 #[test]
576 fn should_convert_basic_results_to_impacts() {
577 let instance1: CloudResource = CloudResource {
578 provider: CloudProvider::AWS,
579 id: "inst-1".to_string(),
580 location: UsageLocation::try_from("eu-west-3").unwrap(),
581 resource_details: ResourceDetails::Instance {
582 instance_type: "m6g.xlarge".to_string(),
583 usage: Some(InstanceUsage {
584 average_cpu_load: 100.0,
585 state: InstanceState::Running,
586 }),
587 },
588 tags: Vec::new(),
589 };
590
591 let raw_impacts =
592 Some(serde_json::from_str(DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR).unwrap());
593 let one_hour: f32 = 1 as f32;
594 let cloud_resource_with_impacts: CloudResourceWithImpacts =
595 boa_impacts_to_cloud_resource_with_impacts(&instance1, &raw_impacts, &one_hour);
596 assert!(
597 cloud_resource_with_impacts.impacts_values.is_some(),
598 "Empty impacts"
599 );
600
601 assert_eq!(
602 0.212,
603 cloud_resource_with_impacts
604 .impacts_values
605 .as_ref()
606 .unwrap()
607 .pe_use_megajoules
608 );
609
610 assert_eq!(
611 0.212,
612 cloud_resource_with_impacts
613 .impacts_values
614 .unwrap()
615 .raw_data
616 .unwrap()["impacts"]["pe"]["use"]["value"]
617 .as_f64()
618 .unwrap()
619 );
620 }
621 #[test]
622 fn convert_verbose_results_to_impacts() {
623 let instance1: CloudResource = CloudResource {
624 provider: CloudProvider::AWS,
625 id: "inst-1".to_string(),
626 location: UsageLocation::try_from("eu-west-3").unwrap(),
627 resource_details: ResourceDetails::Instance {
628 instance_type: "m6g.xlarge".to_string(),
629 usage: Some(InstanceUsage {
630 average_cpu_load: 100.0,
631 state: InstanceState::Running,
632 }),
633 },
634 tags: Vec::new(),
635 };
636
637 let raw_impacts =
638 Some(serde_json::from_str(DEFAULT_RAW_IMPACTS_OF_M6GXLARGE_1HRS_FR_VERBOSE).unwrap());
639 let one_hour: f32 = 1 as f32;
640 let cloud_resource_with_impacts: CloudResourceWithImpacts =
641 boa_impacts_to_cloud_resource_with_impacts(&instance1, &raw_impacts, &one_hour);
642 assert!(
643 cloud_resource_with_impacts.impacts_values.is_some(),
644 "Emtpy impacts"
645 );
646
647 assert_eq!(
648 0.0005454,
649 cloud_resource_with_impacts
650 .impacts_values
651 .unwrap()
652 .raw_data
653 .unwrap()["verbose"]["CPU-1"]["impacts"]["gwp"]["embedded"]["value"]
654 .as_f64()
655 .unwrap()
656 );
657 }
658}