Skip to content

Commit fbea446

Browse files
committed
feat: add Saved Cost DataType
Signed-off-by: ImMin5 <[email protected]>
1 parent d208087 commit fbea446

File tree

2 files changed

+109
-55
lines changed

2 files changed

+109
-55
lines changed

src/cloudforet/cost_analysis/connector/azure_cost_mgmt_connector.py

+9
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ def list_by_billing_account(self):
227227
billing_account_name=billing_account_name
228228
)
229229

230+
def get_retail_price(self, meter_id: str):
231+
url = f"https://prices.azure.com/api/retail/prices?$filter=priceType eq 'Consumption' and meterId eq '{meter_id}'"
232+
try:
233+
response = requests.get(url=url)
234+
return response.json()
235+
except Exception as e:
236+
_LOGGER.error(f"[ERROR] get_retail_price {e}")
237+
raise ERROR_UNKNOWN(message=f"[ERROR] get_retail_price failed {e}")
238+
230239
def _make_request_headers(self, client_type=None):
231240
access_token = self._get_access_token()
232241
headers = {

src/cloudforet/cost_analysis/manager/cost_manager.py

+100-55
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ def __init__(self, *args, **kwargs):
2323
self.azure_cm_connector: AzureCostMgmtConnector = self.locator.get_connector(
2424
"AzureCostMgmtConnector"
2525
)
26+
self.retail_price_map = {}
2627

2728
def get_linked_accounts(
28-
self,
29-
options: dict,
30-
secret_data: dict,
31-
schema: str,
32-
domain_id: str,
29+
self,
30+
options: dict,
31+
secret_data: dict,
32+
schema: str,
33+
domain_id: str,
3334
) -> dict:
3435
self.azure_cm_connector.create_session(options, secret_data, schema)
3536
billing_account_info = self.azure_cm_connector.get_billing_account()
@@ -56,12 +57,12 @@ def get_linked_accounts(
5657
return {"results": accounts_info}
5758

5859
def get_benefit_data(
59-
self,
60-
options: dict,
61-
secret_data: dict,
62-
schema: str,
63-
task_options: dict,
64-
domain_id: str,
60+
self,
61+
options: dict,
62+
secret_data: dict,
63+
schema: str,
64+
task_options: dict,
65+
domain_id: str,
6566
):
6667
self.azure_cm_connector.create_session(options, secret_data, schema)
6768
start: datetime = self._get_first_date_of_month(task_options["start"])
@@ -84,12 +85,12 @@ def get_benefit_data(
8485
)
8586

8687
def _make_benefit_cost_data(
87-
self,
88-
results: dict,
89-
end: datetime,
90-
options: dict,
91-
tenant_id: str = None,
92-
agreement_type: str = None,
88+
self,
89+
results: dict,
90+
end: datetime,
91+
options: dict,
92+
tenant_id: str = None,
93+
agreement_type: str = None,
9394
) -> list:
9495
benefit_costs_data = []
9596
try:
@@ -142,12 +143,12 @@ def _make_benefit_cost_info(self, result: dict, billed_at: str) -> dict:
142143
return data
143144

144145
def get_data(
145-
self,
146-
options: dict,
147-
secret_data: dict,
148-
schema: str,
149-
task_options: dict,
150-
domain_id: str,
146+
self,
147+
options: dict,
148+
secret_data: dict,
149+
schema: str,
150+
task_options: dict,
151+
domain_id: str,
151152
) -> list:
152153
self.azure_cm_connector.create_session(options, secret_data, schema)
153154
self._check_task_options(task_options)
@@ -201,12 +202,12 @@ def get_data(
201202
yield []
202203

203204
def _make_cost_data(
204-
self,
205-
results: list,
206-
end: datetime,
207-
options: dict,
208-
tenant_id: str = None,
209-
agreement_type: str = None,
205+
self,
206+
results: list,
207+
end: datetime,
208+
options: dict,
209+
tenant_id: str = None,
210+
agreement_type: str = None,
210211
) -> list:
211212
"""Source Data Model"""
212213

@@ -247,14 +248,14 @@ def _make_transaction_cost_data(self, tenant_id: str, end: datetime) -> list:
247248

248249
try:
249250
for (
250-
reservation_transaction
251+
reservation_transaction
251252
) in self.azure_cm_connector.list_reservation_transactions_by_billing_profile_id(
252253
query_filter
253254
):
254255

255256
if (
256-
reservation_transaction.invoice_section_id.split("/")[-1]
257-
== invoice_section_id
257+
reservation_transaction.invoice_section_id.split("/")[-1]
258+
== invoice_section_id
258259
):
259260
reservation_transaction_info = (
260261
self.azure_cm_connector.convert_nested_dictionary(
@@ -311,12 +312,12 @@ def _make_transaction_cost_data(self, tenant_id: str, end: datetime) -> list:
311312
return transaction_cost_data
312313

313314
def _make_data_info(
314-
self,
315-
result: dict,
316-
billed_date: str,
317-
options: dict,
318-
tenant_id: str = None,
319-
agreement_type: str = None,
315+
self,
316+
result: dict,
317+
billed_date: str,
318+
options: dict,
319+
tenant_id: str = None,
320+
agreement_type: str = None,
320321
):
321322
additional_info: dict = self._get_additional_info(result, options, tenant_id)
322323
cost: float = self._get_cost_from_result_with_options(result, options)
@@ -390,8 +391,8 @@ def _get_additional_info(self, result: dict, options: dict, tenant_id: str = Non
390391
additional_info["Benefit Name"] = benefit_name
391392

392393
if (
393-
result.get("pricingmodel") == "Reservation"
394-
and result["metercategory"] == ""
394+
result.get("pricingmodel") == "Reservation"
395+
and result["metercategory"] == ""
395396
):
396397
result["metercategory"] = self._set_product_from_benefit_name(
397398
benefit_name
@@ -403,14 +404,14 @@ def _get_additional_info(self, result: dict, options: dict, tenant_id: str = Non
403404
if result.get("metersubcategory") != "" and result.get("metersubcategory"):
404405
additional_info["Meter SubCategory"] = result.get("metersubcategory")
405406
if (
406-
result.get("pricingmodel") == "OnDemand"
407-
and result.get("metercategory") == ""
407+
result.get("pricingmodel") == "OnDemand"
408+
and result.get("metercategory") == ""
408409
):
409410
result["metercategory"] = result.get("metercategory")
410411

411412
if result.get("customername") is None:
412413
if result.get("invoicesectionname") != "" and result.get(
413-
"invoicesectionname"
414+
"invoicesectionname"
414415
):
415416
additional_info["Department Name"] = result.get("invoicesectionname")
416417
elif result.get("departmentname") != "" and result.get("departmentname"):
@@ -419,7 +420,7 @@ def _get_additional_info(self, result: dict, options: dict, tenant_id: str = Non
419420
if result.get("accountname") != "" and result.get("accountname"):
420421
additional_info["Enrollment Account Name"] = result["accountname"]
421422
elif result.get("enrollmentaccountname") != "" and result.get(
422-
"enrollmentaccountname"
423+
"enrollmentaccountname"
423424
):
424425
additional_info["Enrollment Account Name"] = result["enrollmentaccountname"]
425426

@@ -428,9 +429,9 @@ def _get_additional_info(self, result: dict, options: dict, tenant_id: str = Non
428429

429430
collect_resource_id = options.get("collect_resource_id", False)
430431
if (
431-
collect_resource_id
432-
and result.get("resourceid") != ""
433-
and result.get("resourceid")
432+
collect_resource_id
433+
and result.get("resourceid") != ""
434+
and result.get("resourceid")
434435
):
435436
additional_info["Resource Id"] = result["resourceid"]
436437
additional_info["Resource Name"] = result["resourceid"].split("/")[-1]
@@ -488,14 +489,58 @@ def _get_aggregate_data(self, result: dict, options: dict) -> dict:
488489

489490
if result.get("reservationname") != "" and result.get("reservationname"):
490491
aggregate_data["Actual Cost"] = 0
492+
elif result.get("benefitname") != "" and result.get("benefitname"):
493+
aggregate_data["Actual Cost"] = 0
491494
else:
492495
aggregate_data["Actual Cost"] = cost_in_billing_currency
493496

497+
if result.get("pricingmodel") in ["Reservation", "SavingsPlan"]:
498+
aggregate_data["Saved Cost"] = self._get_saved_cost(
499+
result, cost_in_billing_currency
500+
)
501+
494502
else:
495503
aggregate_data["Actual Cost"] = cost_in_billing_currency
496504

497505
return aggregate_data
498506

507+
def _get_saved_cost(self, result: dict, cost: float) -> float:
508+
exchange_rate = 1.0
509+
saved_cost = 0
510+
currency = result.get("billingcurrency", "USD")
511+
meter_id = result.get("meterid")
512+
quantity = self._convert_str_to_float_format(result.get("quantity", 0.0))
513+
514+
if self.retail_price_map.get(meter_id):
515+
unit_price = self.retail_price_map[meter_id]
516+
else:
517+
unit_price = self._get_unit_price_from_meter_id(meter_id)
518+
self.retail_price_map[meter_id] = unit_price
519+
520+
if currency != "USD":
521+
exchange_rate = result.get("exchangeratepricingtobilling", 1.0) or 1.0
522+
523+
retail_cost = exchange_rate * quantity * unit_price
524+
if retail_cost:
525+
saved_cost = retail_cost - cost
526+
527+
return saved_cost
528+
529+
def _get_unit_price_from_meter_id(self, meter_id: str) -> float:
530+
unit_price = 0.0
531+
try:
532+
response = self.azure_cm_connector.get_retail_price(meter_id)
533+
items = response.get("Items", [])
534+
535+
for item in items:
536+
if item.get("meterId") == meter_id:
537+
unit_price = item.get("retailPrice", 0.0)
538+
break
539+
540+
except Exception as e:
541+
_LOGGER.error(f"[_get_unit_price_from_meter_id] get unit price error: {e}")
542+
return unit_price
543+
499544
@staticmethod
500545
def _get_region_code(resource_location: str) -> str:
501546
return resource_location.lower() if resource_location else resource_location
@@ -524,10 +569,10 @@ def _get_tenant_ids(task_options: dict, collect_scope: str) -> list:
524569

525570
@staticmethod
526571
def _make_scope(
527-
secret_data: dict,
528-
task_options: dict,
529-
collect_scope: str,
530-
customer_tenant_id: str = None,
572+
secret_data: dict,
573+
task_options: dict,
574+
collect_scope: str,
575+
customer_tenant_id: str = None,
531576
):
532577
if collect_scope == "subscription_id":
533578
subscription_id = task_options["subscription_id"]
@@ -628,7 +673,7 @@ def _convert_date_format_to_utc(date_format: str) -> datetime:
628673
return datetime.strptime(date_format, "%Y-%m-%d").replace(tzinfo=timezone.utc)
629674

630675
def _make_monthly_time_period(
631-
self, start_date: datetime, end_date: datetime
676+
self, start_date: datetime, end_date: datetime
632677
) -> list:
633678
monthly_time_period = []
634679
current_date = end_date
@@ -658,7 +703,7 @@ def _make_monthly_time_period(
658703

659704
@staticmethod
660705
def _get_linked_customer_tenants(
661-
secret_data: dict, billing_accounts_info: list
706+
secret_data: dict, billing_accounts_info: list
662707
) -> list:
663708
customer_tenants = secret_data.get("customer_tenants", [])
664709
if not customer_tenants:
@@ -671,7 +716,7 @@ def _get_linked_customer_tenants(
671716

672717
@staticmethod
673718
def _make_accounts_info_from_customer_tenants(
674-
billing_accounts_info: list, customer_tenants: list
719+
billing_accounts_info: list, customer_tenants: list
675720
) -> list:
676721
accounts_info = []
677722
for billing_account_info in billing_accounts_info:
@@ -709,7 +754,7 @@ def _exclude_cost_data_with_options(result: dict, options: dict) -> bool:
709754

710755
@staticmethod
711756
def _set_network_traffic_cost(
712-
additional_info: dict, product: str, usage_type: str
757+
additional_info: dict, product: str, usage_type: str
713758
) -> dict:
714759
if product in ["Bandwidth", "Content Delivery Network"]:
715760
additional_info["Usage Type Details"] = usage_type

0 commit comments

Comments
 (0)