From fbea4462d2637b925ff5d5e89df6b9c128a75ace Mon Sep 17 00:00:00 2001 From: ImMin5 Date: Thu, 1 Aug 2024 22:31:41 +0900 Subject: [PATCH] feat: add Saved Cost DataType Signed-off-by: ImMin5 --- .../connector/azure_cost_mgmt_connector.py | 9 + .../cost_analysis/manager/cost_manager.py | 155 +++++++++++------- 2 files changed, 109 insertions(+), 55 deletions(-) diff --git a/src/cloudforet/cost_analysis/connector/azure_cost_mgmt_connector.py b/src/cloudforet/cost_analysis/connector/azure_cost_mgmt_connector.py index ab901d4..a2f7e11 100644 --- a/src/cloudforet/cost_analysis/connector/azure_cost_mgmt_connector.py +++ b/src/cloudforet/cost_analysis/connector/azure_cost_mgmt_connector.py @@ -227,6 +227,15 @@ def list_by_billing_account(self): billing_account_name=billing_account_name ) + def get_retail_price(self, meter_id: str): + url = f"https://prices.azure.com/api/retail/prices?$filter=priceType eq 'Consumption' and meterId eq '{meter_id}'" + try: + response = requests.get(url=url) + return response.json() + except Exception as e: + _LOGGER.error(f"[ERROR] get_retail_price {e}") + raise ERROR_UNKNOWN(message=f"[ERROR] get_retail_price failed {e}") + def _make_request_headers(self, client_type=None): access_token = self._get_access_token() headers = { diff --git a/src/cloudforet/cost_analysis/manager/cost_manager.py b/src/cloudforet/cost_analysis/manager/cost_manager.py index 9b2014e..da7edcf 100644 --- a/src/cloudforet/cost_analysis/manager/cost_manager.py +++ b/src/cloudforet/cost_analysis/manager/cost_manager.py @@ -23,13 +23,14 @@ def __init__(self, *args, **kwargs): self.azure_cm_connector: AzureCostMgmtConnector = self.locator.get_connector( "AzureCostMgmtConnector" ) + self.retail_price_map = {} def get_linked_accounts( - self, - options: dict, - secret_data: dict, - schema: str, - domain_id: str, + self, + options: dict, + secret_data: dict, + schema: str, + domain_id: str, ) -> dict: self.azure_cm_connector.create_session(options, secret_data, schema) billing_account_info = self.azure_cm_connector.get_billing_account() @@ -56,12 +57,12 @@ def get_linked_accounts( return {"results": accounts_info} def get_benefit_data( - self, - options: dict, - secret_data: dict, - schema: str, - task_options: dict, - domain_id: str, + self, + options: dict, + secret_data: dict, + schema: str, + task_options: dict, + domain_id: str, ): self.azure_cm_connector.create_session(options, secret_data, schema) start: datetime = self._get_first_date_of_month(task_options["start"]) @@ -84,12 +85,12 @@ def get_benefit_data( ) def _make_benefit_cost_data( - self, - results: dict, - end: datetime, - options: dict, - tenant_id: str = None, - agreement_type: str = None, + self, + results: dict, + end: datetime, + options: dict, + tenant_id: str = None, + agreement_type: str = None, ) -> list: benefit_costs_data = [] try: @@ -142,12 +143,12 @@ def _make_benefit_cost_info(self, result: dict, billed_at: str) -> dict: return data def get_data( - self, - options: dict, - secret_data: dict, - schema: str, - task_options: dict, - domain_id: str, + self, + options: dict, + secret_data: dict, + schema: str, + task_options: dict, + domain_id: str, ) -> list: self.azure_cm_connector.create_session(options, secret_data, schema) self._check_task_options(task_options) @@ -201,12 +202,12 @@ def get_data( yield [] def _make_cost_data( - self, - results: list, - end: datetime, - options: dict, - tenant_id: str = None, - agreement_type: str = None, + self, + results: list, + end: datetime, + options: dict, + tenant_id: str = None, + agreement_type: str = None, ) -> list: """Source Data Model""" @@ -247,14 +248,14 @@ def _make_transaction_cost_data(self, tenant_id: str, end: datetime) -> list: try: for ( - reservation_transaction + reservation_transaction ) in self.azure_cm_connector.list_reservation_transactions_by_billing_profile_id( query_filter ): if ( - reservation_transaction.invoice_section_id.split("/")[-1] - == invoice_section_id + reservation_transaction.invoice_section_id.split("/")[-1] + == invoice_section_id ): reservation_transaction_info = ( self.azure_cm_connector.convert_nested_dictionary( @@ -311,12 +312,12 @@ def _make_transaction_cost_data(self, tenant_id: str, end: datetime) -> list: return transaction_cost_data def _make_data_info( - self, - result: dict, - billed_date: str, - options: dict, - tenant_id: str = None, - agreement_type: str = None, + self, + result: dict, + billed_date: str, + options: dict, + tenant_id: str = None, + agreement_type: str = None, ): additional_info: dict = self._get_additional_info(result, options, tenant_id) 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 additional_info["Benefit Name"] = benefit_name if ( - result.get("pricingmodel") == "Reservation" - and result["metercategory"] == "" + result.get("pricingmodel") == "Reservation" + and result["metercategory"] == "" ): result["metercategory"] = self._set_product_from_benefit_name( benefit_name @@ -403,14 +404,14 @@ def _get_additional_info(self, result: dict, options: dict, tenant_id: str = Non if result.get("metersubcategory") != "" and result.get("metersubcategory"): additional_info["Meter SubCategory"] = result.get("metersubcategory") if ( - result.get("pricingmodel") == "OnDemand" - and result.get("metercategory") == "" + result.get("pricingmodel") == "OnDemand" + and result.get("metercategory") == "" ): result["metercategory"] = result.get("metercategory") if result.get("customername") is None: if result.get("invoicesectionname") != "" and result.get( - "invoicesectionname" + "invoicesectionname" ): additional_info["Department Name"] = result.get("invoicesectionname") 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 if result.get("accountname") != "" and result.get("accountname"): additional_info["Enrollment Account Name"] = result["accountname"] elif result.get("enrollmentaccountname") != "" and result.get( - "enrollmentaccountname" + "enrollmentaccountname" ): additional_info["Enrollment Account Name"] = result["enrollmentaccountname"] @@ -428,9 +429,9 @@ def _get_additional_info(self, result: dict, options: dict, tenant_id: str = Non collect_resource_id = options.get("collect_resource_id", False) if ( - collect_resource_id - and result.get("resourceid") != "" - and result.get("resourceid") + collect_resource_id + and result.get("resourceid") != "" + and result.get("resourceid") ): additional_info["Resource Id"] = result["resourceid"] additional_info["Resource Name"] = result["resourceid"].split("/")[-1] @@ -488,14 +489,58 @@ def _get_aggregate_data(self, result: dict, options: dict) -> dict: if result.get("reservationname") != "" and result.get("reservationname"): aggregate_data["Actual Cost"] = 0 + elif result.get("benefitname") != "" and result.get("benefitname"): + aggregate_data["Actual Cost"] = 0 else: aggregate_data["Actual Cost"] = cost_in_billing_currency + if result.get("pricingmodel") in ["Reservation", "SavingsPlan"]: + aggregate_data["Saved Cost"] = self._get_saved_cost( + result, cost_in_billing_currency + ) + else: aggregate_data["Actual Cost"] = cost_in_billing_currency return aggregate_data + def _get_saved_cost(self, result: dict, cost: float) -> float: + exchange_rate = 1.0 + saved_cost = 0 + currency = result.get("billingcurrency", "USD") + meter_id = result.get("meterid") + quantity = self._convert_str_to_float_format(result.get("quantity", 0.0)) + + if self.retail_price_map.get(meter_id): + unit_price = self.retail_price_map[meter_id] + else: + unit_price = self._get_unit_price_from_meter_id(meter_id) + self.retail_price_map[meter_id] = unit_price + + if currency != "USD": + exchange_rate = result.get("exchangeratepricingtobilling", 1.0) or 1.0 + + retail_cost = exchange_rate * quantity * unit_price + if retail_cost: + saved_cost = retail_cost - cost + + return saved_cost + + def _get_unit_price_from_meter_id(self, meter_id: str) -> float: + unit_price = 0.0 + try: + response = self.azure_cm_connector.get_retail_price(meter_id) + items = response.get("Items", []) + + for item in items: + if item.get("meterId") == meter_id: + unit_price = item.get("retailPrice", 0.0) + break + + except Exception as e: + _LOGGER.error(f"[_get_unit_price_from_meter_id] get unit price error: {e}") + return unit_price + @staticmethod def _get_region_code(resource_location: str) -> str: 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: @staticmethod def _make_scope( - secret_data: dict, - task_options: dict, - collect_scope: str, - customer_tenant_id: str = None, + secret_data: dict, + task_options: dict, + collect_scope: str, + customer_tenant_id: str = None, ): if collect_scope == "subscription_id": subscription_id = task_options["subscription_id"] @@ -628,7 +673,7 @@ def _convert_date_format_to_utc(date_format: str) -> datetime: return datetime.strptime(date_format, "%Y-%m-%d").replace(tzinfo=timezone.utc) def _make_monthly_time_period( - self, start_date: datetime, end_date: datetime + self, start_date: datetime, end_date: datetime ) -> list: monthly_time_period = [] current_date = end_date @@ -658,7 +703,7 @@ def _make_monthly_time_period( @staticmethod def _get_linked_customer_tenants( - secret_data: dict, billing_accounts_info: list + secret_data: dict, billing_accounts_info: list ) -> list: customer_tenants = secret_data.get("customer_tenants", []) if not customer_tenants: @@ -671,7 +716,7 @@ def _get_linked_customer_tenants( @staticmethod def _make_accounts_info_from_customer_tenants( - billing_accounts_info: list, customer_tenants: list + billing_accounts_info: list, customer_tenants: list ) -> list: accounts_info = [] for billing_account_info in billing_accounts_info: @@ -709,7 +754,7 @@ def _exclude_cost_data_with_options(result: dict, options: dict) -> bool: @staticmethod def _set_network_traffic_cost( - additional_info: dict, product: str, usage_type: str + additional_info: dict, product: str, usage_type: str ) -> dict: if product in ["Bandwidth", "Content Delivery Network"]: additional_info["Usage Type Details"] = usage_type