diff --git a/cost/aws/savings_plan/recommendations/CHANGELOG.md b/cost/aws/savings_plan/recommendations/CHANGELOG.md index fbddf1ab26..67eccb7588 100644 --- a/cost/aws/savings_plan/recommendations/CHANGELOG.md +++ b/cost/aws/savings_plan/recommendations/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v3.0 + +- Policy now automatically converts savings to local currency when appropriate +- Removed parameter to do the above manually via a user-specified exchange rate +- Added exchange rate context to incident to allow user to derive unmodified values when needed +- Several parameters altered to be more descriptive and intuitive to use +- Added additional context to incident description +- Normalized incident export to be consistent with other policies +- Streamlined code for better readability and faster execution +- Policy now requires a valid Flexera credential + ## v2.17 - Added `Term` incident field. diff --git a/cost/aws/savings_plan/recommendations/README.md b/cost/aws/savings_plan/recommendations/README.md index 16e23cfcf7..0d328055dd 100644 --- a/cost/aws/savings_plan/recommendations/README.md +++ b/cost/aws/savings_plan/recommendations/README.md @@ -1,55 +1,68 @@ # AWS Savings Plan Recommendations -This Policy Template leverages the [AWS Savings Plans Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetSavingsPlansPurchaseRecommendation.html). It will raise incidents if AWS has any Savings Plan Purchase Recommendations, whose monthly savings exceeds the *Monthly Savings Threshold* parameter in the Policy. -It will email the user specified in `Email addresses of the recipients you wish to notify` +## What it does -> *NOTE: This Policy Template must be appled to the **AWS Organization Master Payer** account.* +This Policy Template reports any Savings Plan Purchase Recommendations generated by AWS. The user can adjust which recommendations are reported via policy parameters. + +> *NOTE: This Policy Template must be applied to the **AWS Organization Master Payer** account.* + +## Functional Details + +Recommendations are obtained via requests to the [AWS Savings Plans Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetSavingsPlansPurchaseRecommendation.html). + +### Policy Savings Details + +The policy includes the estimated savings. The estimated savings is recognized if the recommended savings plan is purchased. The savings values are provided directly by the [AWS Savings Plans Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetSavingsPlansPurchaseRecommendation.html). + +If the Flexera organization is configured to use a currency other than the one the [AWS Savings Plans Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetSavingsPlansPurchaseRecommendation.html) returns, the savings values will be converted using the exchange rate at the time that the policy executes. ## Input Parameters This policy has the following input parameters required when launching the policy. -- *Look Back Period* - Specify the number of days of past usage to analyze. -- *Savings Plan Term* - Specify he Term length for the Savings Plan. +- *Email Addresses* - Email addresses of the recipients you wish to notify when new incidents are created. - *Account Number* - The Account number for use with the AWS STS Cross Account Role. Leave blank when using AWS IAM Access key and secret. It only needs to be passed when the desired AWS account is different than the one associated with the Flexera One credential. [more](https://docs.flexera.com/flexera/EN/Automation/ProviderCredentials.htm#automationadmin_1982464505_1123608) -- *Payment Option* - Specify the payment option for the Savings Plan. -- *Savings Plan Type* - Choose between Compute Savings Plans, EC2 Instance Savings Plans or SageMaker Savings Plans -- *Monthly Savings Threshold* - Specify the minimum monthly savings that should result in a Savings Plan purchase recommendation -- *Email addresses to notify* - A list of email addresses to notify -- *Currency Adjustment* - Number to adjust monthly estimated savings by depending on USD conversion (maximum value 5.0) +- *Minimum Savings Threshold* - Minimum potential savings required to generate a recommendation. +- *Look Back Period* - Number of days of prior usage to analyze +- *Account Scope* - The account scope that you want your recommendations for. Select Payer to produce results only for a Master Payer account, or Linked to produce results for all linked accounts as well. +- *Savings Plan Term* - Length of savings plan term to provide recommendations for. +- *Savings Plan Type* - Type of Savings Plan to provide recommendations for. +- *Payment Option* - Savings Plan purchase option to provide recommendations for. ## Policy Actions +The following policy actions are taken on any resources found to be out of compliance. + - Send an email report ## Prerequisites -This policy uses [credentials](https://docs.flexera.com/flexera/EN/Automation/ManagingCredentialsExternal.htm) for connecting to the cloud -- in order to apply this policy you must have a credential registered in the system that is compatible with this policy. If there are no credentials listed when you apply the policy, please contact your cloud admin and ask them to register a credential that is compatible with this policy. The information below should be consulted when creating the credential. +This Policy Template uses [Credentials](https://docs.flexera.com/flexera/EN/Automation/ManagingCredentialsExternal.htm) for authenticating to datasources -- in order to apply this policy you must have a Credential registered in the system that is compatible with this policy. If there are no Credentials listed when you apply the policy, please contact your Flexera Org Admin and ask them to register a Credential that is compatible with this policy. The information below should be consulted when creating the credential(s). -## Credential configuration +- [**AWS Credential**](https://docs.flexera.com/flexera/EN/Automation/ProviderCredentials.htm#automationadmin_1982464505_1121575) (*provider=aws*) which has the following permissions: + - `ce:GetSavingsPlansPurchaseRecommendation` -For administrators [creating and managing credentials](https://docs.flexera.com/flexera/EN/Automation/ManagingCredentialsExternal.htm) to use with this policy, the following information is needed: + Example IAM Permission Policy: -Provider tag value to match this policy: `aws` + ```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ce:GetSavingsPlansPurchaseRecommendation" + ], + "Resource": "*" + } + ] + } + ``` -Required permissions in the provider: +- [**Flexera Credential**](https://docs.flexera.com/flexera/EN/Automation/ProviderCredentials.htm) (*provider=flexera*) which has the following roles: + - `billing_center_viewer` -```javascript -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "ce:*" - ], - "Resource": [ - "*" - ] - } - ] -} -``` +The [Provider-Specific Credentials](https://docs.flexera.com/flexera/EN/Automation/ProviderCredentials.htm) page in the docs has detailed instructions for setting up Credentials for the most common providers. ## Supported Clouds diff --git a/cost/aws/savings_plan/recommendations/aws_savings_plan_recommendations.pt b/cost/aws/savings_plan/recommendations/aws_savings_plan_recommendations.pt index 2544dbe96f..772964e04a 100644 --- a/cost/aws/savings_plan/recommendations/aws_savings_plan_recommendations.pt +++ b/cost/aws/savings_plan/recommendations/aws_savings_plan_recommendations.pt @@ -1,15 +1,15 @@ name "AWS Savings Plan Recommendations" rs_pt_ver 20180301 type "policy" -short_description "A policy that sends email notifications when AWS Savings Plan Recommendations are identified. NOTE: These Recommendations are generated by AWS. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/aws/savings_plan/recommendations) and [docs.flexera.com/flexera/EN/Automation](https://docs.flexera.com/flexera/EN/Automation/AutomationGS.htm) to learn more." +short_description "A policy that sends email notifications when AWS Savings Plan Recommendations are identified. NOTE: These Purchase Recommendations are generated by AWS. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/aws/savings_plan/recommendations) and [docs.flexera.com/flexera/EN/Automation](https://docs.flexera.com/flexera/EN/Automation/AutomationGS.htm) to learn more." long_description "" severity "medium" category "Cost" -default_frequency "daily" +default_frequency "weekly" info( - version: "2.17", + version: "3.0", provider: "AWS", - service: "", + service: "Compute", policy_set: "Savings Plans", recommendation_type: "Rate Reduction" ) @@ -18,84 +18,80 @@ info( # Parameters ############################################################################## -parameter "param_days" do - category "Savings Plan" - label "Look Back Period" - default "Last 7 Days" - description "Number of days of prior usage to analyze" - allowed_values "Last 7 Days","Last 30 Days","Last 60 Days" - type "string" +parameter "param_email" do + type "list" + category "Policy Settings" + label "Email Addresses" + description "Email addresses of the recipients you wish to notify when new incidents are created." + default [] end -parameter "param_term" do - category "Savings Plan" - label "Savings Plan Term" - description "Length of Savings Plan term" - allowed_values "1 Year","3 Year" - default "1 Year" +parameter "param_aws_account_number" do type "string" + category "Policy Settings" + label "Account Number" + description "Leave blank; this is for automated use with Meta Policies. See README for more details." + default "" end -parameter "param_payment_option" do - category "Savings Plan" - label "Payment Option" - default "No Upfront" - allowed_values "No Upfront","Partial Upfront","All Upfront","Light Utilization","Medium Utilization","Heavy Utilization" - type "string" - description "Savings Plan purchase option." +parameter "param_min_savings" do + type "number" + category "Policy Settings" + label "Minimum Savings Threshold" + description "Specify the minimum estimated monthly savings that should result in a recommendation" + default 0 + min_value 0 end -parameter "param_aws_account_number" do +parameter "param_days" do type "string" - label "Account Number" - description "The account number for AWS STS Cross Account Roles." - default "" + category "Savings Plan Settings" + label "Look Back Period" + description "Number of days of prior usage to analyze." + default "Last 30 Days" + allowed_values "Last 7 Days", "Last 30 Days", "Last 60 Days" end parameter "param_scope" do - category "Savings Plan" + type "string" + category "Savings Plan Settings" label "Account Scope" - description "The account scope that you want your recommendations for" - allowed_values "Payer","Linked" + description "The account scope that you want your recommendations for. Select Payer to produce results only for a Master Payer account, or Linked to produce results for all linked accounts as well." default "Payer" + allowed_values "Payer", "Linked" +end + +parameter "param_term" do type "string" + category "Savings Plan Settings" + label "Savings Plan Term" + description "Length of savings plan term to provide recommendations for." + default "1 Year" + allowed_values "Any", "1 Year", "3 Year" end parameter "param_savings_plan_type" do - category "Savings Plan" + type "string" + category "Savings Plan Settings" label "Savings Plan Type" + description "Type of Savings Plan to provide recommendations for." default "Compute Savings Plan" - allowed_values "Compute Savings Plan","EC2 Instance Savings Plan","SageMaker Savings Plans" - type "string" -end - -parameter "param_savings_threshold" do - category "Savings Plan" - label "Monthly Savings Threshold" - description "Specify the minimum estimated monthly savings that should result in a recommendation" - default 100 - type "number" -end - -parameter "param_email" do - type "list" - label "Email addresses to notify" - description "Email addresses of the recipients you wish to notify" + allowed_values "Compute Savings Plan", "EC2 Instance Savings Plan", "SageMaker Savings Plans" end -parameter "param_currency_adjustment" do - label "Currency Adjustment" - description "adjusts recommendation savings based on USD conversion rate where USD is 100%" - default 1.00 - max_value 5.00 - type "number" +parameter "param_payment_option" do + type "string" + category "Savings Plan Settings" + label "Payment Option" + description "Savings Plan purchase option to provide recommendations for." + default "No Upfront" + allowed_values "No Upfront", "Partial Upfront", "All Upfront" end ############################################################################### # Authentication ############################################################################### -#Authenticate with AWS credentials "auth_aws" do schemes "aws","aws_sts" label "AWS" @@ -106,7 +102,7 @@ end credentials "auth_flexera" do schemes "oauth2" - label "flexera" + label "Flexera" description "Select FlexeraOne OAuth2 credentials" tags "provider=flexera" end @@ -115,7 +111,7 @@ end # Pagination ############################################################################### -pagination "aws_pagination" do +pagination "pagination_aws" do get_page_marker do body_path "NextPageToken" end @@ -125,276 +121,455 @@ pagination "aws_pagination" do end ############################################################################### -# Datasources +# Datasources & Scripts ############################################################################### -datasource "ds_get_caller_identity" do +# Get applied policy metadata for use later +datasource "ds_applied_policy" do request do - auth $auth_aws - verb "GET" - host "sts.amazonaws.com" - path "/" - header "User-Agent", "RS Policies" - query "Action", "GetCallerIdentity" - query "Version", "2011-06-15" + auth $auth_flexera + host rs_governance_host + path join(["/api/governance/projects/", rs_project_id, "/applied_policies/", policy_id]) + header "Api-Version", "1.0" end - result do - encoding "xml" - collect xpath(response, "//GetCallerIdentityResponse/GetCallerIdentityResult") do - field "account",xpath(col_item, "Account") - end +end + +# Gather local currency info +datasource "ds_currency_reference" do + request do + host "raw.githubusercontent.com" + path "/rightscale/policy_templates/master/cost/scheduled_reports/currency_reference.json" + header "User-Agent", "RS Policies" end end -datasource "ds_billing_centers" do +datasource "ds_currency_code" do request do auth $auth_flexera host rs_optima_host - path join(["/analytics/orgs/", rs_org_id, "/billing_centers"]) - header "Api-Version", "1.0" + path join(["/bill-analysis/orgs/", rs_org_id, "/settings/currency_code"]) + header "Api-Version", "0.1" header "User-Agent", "RS Policies" - query "view", "allocation_table" - ignore_status [403] + ignore_status [403] end result do encoding "json" - collect jmes_path(response, "[*]") do - field "href", jmes_path(col_item, "href") - field "id", jmes_path(col_item, "id") - field "name", jmes_path(col_item, "name") - field "parent_id", jmes_path(col_item, "parent_id") - end + field "id", jmes_path(response, "id") + field "value", jmes_path(response, "value") end end -datasource "ds_top_level_billing_centers" do - run_script $js_top_level_bc, $ds_billing_centers +datasource "ds_currency_target" do + run_script $js_currency_target, $ds_currency_reference, $ds_currency_code +end + +script "js_currency_target", type:"javascript" do + parameters "ds_currency_reference", "ds_currency_code" + result "result" + code <<-EOS + // Default to USD if currency is not found + result = ds_currency_reference['USD'] + + if (ds_currency_code['value'] != undefined && ds_currency_reference[ds_currency_code['value']] != undefined) { + result = ds_currency_reference[ds_currency_code['value']] + } +EOS end -datasource "ds_account_name" do +# Get region-specific Flexera API endpoints +datasource "ds_flexera_api_hosts" do + run_script $js_flexera_api_hosts, rs_optima_host +end + +script "js_flexera_api_hosts", type: "javascript" do + parameters "rs_optima_host" + result "result" + code <<-EOS + host_table = { + "api.optima.flexeraeng.com": { + flexera: "api.flexera.com", + fsm: "api.fsm.flexeraeng.com" + }, + "api.optima-eu.flexeraeng.com": { + flexera: "api.flexera.eu", + fsm: "api.fsm-eu.flexeraeng.com" + }, + "api.optima-au.flexeraeng.com": { + flexera: "api.flexera.au", + fsm: "api.fsm-au.flexeraeng.com" + } + } + + result = host_table[rs_optima_host] +EOS +end + +# Get AWS account info +datasource "ds_cloud_vendor_accounts" do request do - run_script $js_get_account_name, $ds_get_caller_identity, $ds_top_level_billing_centers, rs_org_id, rs_optima_host + auth $auth_flexera + host val($ds_flexera_api_hosts, 'flexera') + path join(["/finops-analytics/v1/orgs/", rs_org_id, "/cloud-vendor-accounts"]) + header "Api-Version", "1.0" end result do encoding "json" - collect jmes_path(response, "rows[*]") do - field "vendorAccountName", jmes_path(col_item, "dimensions.vendor_account_name") + collect jmes_path(response, "values[*]") do + field "id", jmes_path(col_item, "aws.accountId") + field "name", jmes_path(col_item, "name") + field "tags", jmes_path(col_item, "tags") end end end +datasource "ds_vendor_account_table" do + run_script $js_vendor_account_table, $ds_cloud_vendor_accounts +end + +script "js_vendor_account_table", type:"javascript" do + parameters "ds_cloud_vendor_accounts" + result "result" + code <<-EOS + result = {} + + _.each(ds_cloud_vendor_accounts, function(account) { + result[account['id']] = account['name'] + }) +EOS +end + datasource "ds_sp_recommendations" do request do - run_script $js_sp_request, $param_days, $param_payment_option, $param_term, $param_savings_plan_type, $param_scope + run_script $js_sp_recommendations, $param_days, $param_payment_option, $param_term, $param_savings_plan_type, $param_scope end result do encoding "json" collect jmes_path(response, "SavingsPlansPurchaseRecommendation.SavingsPlansPurchaseRecommendationDetails[*]") do - field "accountID", jmes_path(col_item,"AccountId") - field "currencyCode", jmes_path(col_item,"CurrencyCode") - field "currentAverageHourlyOnDemandSpend", jmes_path(col_item,"CurrentAverageHourlyOnDemandSpend") - field "currentMaximumHourlyOnDemandSpend", jmes_path(col_item,"CurrentMaximumHourlyOnDemandSpend") - field "currentMinimumHourlyOnDemandSpend", jmes_path(col_item,"CurrentMinimumHourlyOnDemandSpend") - field "estimatedAverageUtilization", jmes_path(col_item,"EstimatedAverageUtilization") - field "estimatedMonthlySavingsAmount", jmes_path(col_item,"EstimatedMonthlySavingsAmount") - field "estimatedOnDemandCost", jmes_path(col_item,"EstimatedOnDemandCost") - field "estimatedROI", jmes_path(col_item,"EstimatedROI") - field "estimatedSavingsAmount", jmes_path(col_item,"EstimatedSavingsAmount") - field "estimatedSavingsPercentage", jmes_path(col_item,"EstimatedSavingsPercentage") - field "estimatedSavingsPlanCost", jmes_path(col_item,"EstimatedSPCost") - field "hourlyCommitmentToPurchase", jmes_path(col_item,"HourlyCommitmentToPurchase") - field "instanceFamily", jmes_path(col_item,"SavingsPlansDetails.InstanceFamily") - field "offeringId", jmes_path(col_item,"SavingsPlansDetails.OfferingId") - field "region", jmes_path(col_item,"SavingsPlansDetails.Region") - field "upfrontCost", jmes_path(col_item,"UpfrontCost") + field "accountID", jmes_path(col_item, "AccountId") + field "currency", jmes_path(col_item, "CurrencyCode") + field "currentAverageHourlyOnDemandSpend", jmes_path(col_item, "CurrentAverageHourlyOnDemandSpend") + field "currentMaximumHourlyOnDemandSpend", jmes_path(col_item, "CurrentMaximumHourlyOnDemandSpend") + field "currentMinimumHourlyOnDemandSpend", jmes_path(col_item, "CurrentMinimumHourlyOnDemandSpend") + field "estimatedAverageUtilization", jmes_path(col_item, "EstimatedAverageUtilization") + field "estimatedMonthlySavingsAmount", jmes_path(col_item, "EstimatedMonthlySavingsAmount") + field "estimatedOnDemandCost", jmes_path(col_item, "EstimatedOnDemandCost") + field "estimatedROI", jmes_path(col_item, "EstimatedROI") + field "estimatedSavingsAmount", jmes_path(col_item, "EstimatedSavingsAmount") + field "estimatedSavingsPercentage", jmes_path(col_item, "EstimatedSavingsPercentage") + field "estimatedSavingsPlanCost", jmes_path(col_item, "EstimatedSPCost") + field "hourlyCommitmentToPurchase", jmes_path(col_item, "HourlyCommitmentToPurchase") + field "instanceFamily", jmes_path(col_item, "SavingsPlansDetails.InstanceFamily") + field "offeringId", jmes_path(col_item, "SavingsPlansDetails.OfferingId") + field "region", jmes_path(col_item, "SavingsPlansDetails.Region") + field "upfrontCost", jmes_path(col_item, "UpfrontCost") end end end -datasource "ds_sp_normalization" do - run_script $js_sp_cleanup, $ds_sp_recommendations, $param_currency_adjustment, $ds_account_name, $param_term, $param_payment_option, $param_savings_plan_type, $param_days -end - -############################################################################### -# Scripts -############################################################################### -script "js_get_account_name", type: "javascript" do - parameters "account_id", "billing_centers", "org", "optima_host" +script "js_sp_recommendations", type: "javascript" do + parameters "param_days", "param_payment_option", "param_term", "param_savings_plan_type", "param_scope" result "request" code <<-EOS - // returns date formatted as string: YYYY-mm-dd - function getFormattedDailyDate(date) { - var year = date.getFullYear(); - var month = (1 + date.getMonth()).toString(); - month = month.length > 1 ? month : '0' + month; - var day = date.getDate().toString(); - day = day.length > 1 ? day : '0' + day; - return year + '-' + month + '-' + day; - } - var start_date = getFormattedDailyDate(new Date(new Date().setDate(new Date().getDate() - 3))); - var end_date = getFormattedDailyDate(new Date(new Date().setDate(new Date().getDate() - 2))); - var request = { - auth: "auth_flexera", - host: optima_host, - verb: "POST", - path: "/bill-analysis/orgs/" + org + "/costs/select", - body_fields: { - "dimensions": ["vendor_account_name"], - "granularity": "day", - "start_at": start_date, - "end_at": end_date, - "metrics": ["cost_nonamortized_unblended_adj"], - "billing_center_ids": _.compact(_.map(billing_centers, function(value){ return value.id})), - "limit": 1, - "filter": { - "dimension": "vendor_account", - "type": "equal", - "value": account_id[0]["account"] - } - }, - headers: { - "User-Agent": "RS Policies", - "Api-Version": "1.0" - }, - ignore_status: [400] + // Tables to convert human-readable parameter values to their API equivalents + period_table = { + "Last 7 Days": "SEVEN_DAYS", + "Last 30 Days": "THIRTY_DAYS", + "Last 60 Days": "SIXTY_DAYS" + } + + term_table = { + "1 Year": "ONE_YEAR", + "3 Year": "THREE_YEARS" + } + + plan_table = { + "Compute Savings Plan": "COMPUTE_SP", + "EC2 Instance Savings Plan": "EC2_INSTANCE_SP", + "SageMaker Savings Plans": "SAGEMAKER_SP" + } + + // Build out the body of the request based on parameters + body_fields = { + AccountScope: param_scope.toUpperCase(), + LookbackPeriodInDays: period_table[param_days], + PaymentOption: param_payment_option.replace(' ', '_').toUpperCase() + SavingsPlansType: plan_table[param_savings_plan_type], + TermInYears: term_table[param_term] + } + + var request = { + auth: "auth_aws", + pagination: "pagination_aws", + host: "ce.us-east-1.amazonaws.com", + path: "/", + verb: "POST", + body_fields: body_fields, + headers: { + "User-Agent": "RS Policies", + "X-Amz-Target": "AWSInsightsIndexService.GetSavingsPlansPurchaseRecommendation", + "Content-Type": "application/x-amz-json-1.1", } - EOS + } +EOS end -script "js_top_level_bc", type: "javascript" do - parameters "billing_centers" - result "filtered_billing_centers" - code <<-EOS - var filtered_billing_centers =_.reject(billing_centers, function(bc){ return bc.parent_id != null }); - EOS +# Branching logic: +# This datasource returns an empty array if the target currency is USD. +# This prevents ds_currency_conversion from running if it's not needed. +datasource "ds_conditional_currency_conversion" do + run_script $js_conditional_currency_conversion, $ds_currency_target, $ds_sp_recommendations end -script "js_sp_cleanup", type: "javascript" do - parameters "sp_recos", "currency_adjustment", "ds_account_name", "param_term", "param_payment_option", "param_savings_plan_type", "param_days" +script "js_conditional_currency_conversion", type: "javascript" do + parameters "ds_currency_target", "ds_sp_recommendations" result "result" code <<-EOS - var result = []; - accountName = "" - if (ds_account_name[0] != null) { - accountName = ds_account_name[0]['vendorAccountName'] + result = [] + from_currency = "USD" + + if (ds_sp_recommendations.length > 0) { + if (typeof(ds_sp_recommendations[0]['currency']) == 'string') { + from_currency = ds_sp_recommendations[0]['currency'] } - _.each(sp_recos, function(reco){ - var resourceType = reco["instanceFamily"] - if (resourceType == "" || resourceType == null) { - resourceType = param_savings_plan_type - } - result.push({ - accountID: reco["accountID"], - accountName: accountName, - currencyCode: reco["currencyCode"], - currentAverageHourlyOnDemandSpend: (Math.round(reco["currentAverageHourlyOnDemandSpend"] * 100) / 100).toString(10), - currentMaximumHourlyOnDemandSpend: (Math.round(reco["currentMaximumHourlyOnDemandSpend"] * 100) / 100).toString(10), - currentMinimumHourlyOnDemandSpend: (Math.round(reco["currentMinimumHourlyOnDemandSpend"] * 100) / 100).toString(10), - estimatedAverageUtilization: (Math.round(reco["estimatedAverageUtilization"] * 100) / 100).toString(10), - estimatedMonthlySavingsAmount: (Math.round(reco["estimatedMonthlySavingsAmount"] * 100) / 100 * currency_adjustment).toString(10), - estimatedOnDemandCost: (Math.round(reco["estimatedOnDemandCost"] * 100) / 100).toString(10), - estimatedROI: (Math.round(reco["estimatedROI"] * 100) / 100).toString(10), - estimatedSavingsAmount: (Math.round(reco["estimatedSavingsAmount"] * 100) / 100).toString(10), - estimatedSavingsPercentage: (Math.round(reco["estimatedSavingsPercentage"] * 100) / 100).toString(10), - estimatedSavingsPlanCost: (Math.round(reco["estimatedSavingsPlanCost"] * 100) / 100).toString(10), - hourlyCommitmentToPurchase: (Math.round(reco["hourlyCommitmentToPurchase"] * 100) / 100).toString(10), - instanceFamily: reco["instanceFamily"], - offeringId: reco["offeringId"], - region: reco["region"], - upfrontCost: (Math.round(reco["upfrontCost"] * 100) / 100).toString(10), - estimatedMonthlySavingsAmountWithCurrencyCode : (Math.round(reco["estimatedMonthlySavingsAmount"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - estimatedSavingsPlanCostWithCurrencyCode : (Math.round(reco["estimatedSavingsPlanCost"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - hourlyCommitmentToPurchaseWithCurrencyCode : (Math.round(reco["hourlyCommitmentToPurchase"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - upfrontCostWithCurrencyCode : (Math.round(reco["upfrontCost"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - currentAverageHourlyOnDemandSpendWithCurrencyCode : (Math.round(reco["currentAverageHourlyOnDemandSpend"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - currentMaximumHourlyOnDemandSpendWithCurrencyCode : (Math.round(reco["currentMaximumHourlyOnDemandSpend"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - currentMinimumHourlyOnDemandSpendWithCurrencyCode : (Math.round(reco["currentMinimumHourlyOnDemandSpend"] * 100) / 100).toString(10) + " " + reco["currencyCode"], - term: param_term, - paymentOption: param_payment_option, - resourceType: resourceType, - lookbackPeriod: param_days - }) - }) + } + + // Make the request only if the target currency is not USD + if (ds_currency_target['code'] != from_currency) { + result = [{ from: from_currency }] + } EOS end +datasource "ds_currency_conversion" do + # Only make a request if the target currency is not USD + iterate $ds_conditional_currency_conversion + request do + host "api.xe-auth.flexeraeng.com" + path "/prod/{proxy+}" + query "from", val(iter_item, 'from') + query "to", val($ds_currency_target, 'code') + query "amount", "1" + # Ignore currency conversion if API has issues + ignore_status [400, 404, 502] + end + result do + encoding "json" + field "from", jmes_path(response, "from") + field "to", jmes_path(response, "to") + field "amount", jmes_path(response, "amount") + field "year", jmes_path(response, "year") + end +end -script "js_sp_request", type: "javascript" do - parameters "param_days","param_payment_option","param_term","param_savings_plan_type", "param_scope" - result "request" +datasource "ds_currency" do + run_script $js_currency, $ds_currency_target, $ds_currency_conversion +end + +script "js_currency", type:"javascript" do + parameters "ds_currency_target", "ds_currency_conversion" + result "result" code <<-EOS + result = ds_currency_target + result['exchange_rate'] = 1 - var period = { - "Last 7 Days":"SEVEN_DAYS", - "Last 30 Days":"THIRTY_DAYS", - "Last 60 Days":"SIXTY_DAYS" - } + if (ds_currency_conversion.length > 0) { + currency_code = ds_currency_target['code'] + current_month = parseInt(new Date().toISOString().split('-')[1]) - var term = { - "1 Year":"ONE_YEAR", - "3 Year":"THREE_YEARS" - } + conversion_block = _.find(ds_currency_conversion[0]['to'][currency_code], function(item) { + return item['month'] == current_month + }) - var scope = { - "Payer":"PAYER", - "Linked":"LINKED" + if (conversion_block != undefined) { + result['exchange_rate'] = conversion_block['monthlyAverage'] } + } +EOS +end + - var option = { - "No Upfront":"NO_UPFRONT", - "Partial Upfront":"PARTIAL_UPFRONT", - "All Upfront":"ALL_UPFRONT", - "Light Utilization":"LIGHT_UTILIZATION", - "Medium Utilization":"MEDIUM_UTILIZATION", - "Heavy Utilization":"HEAVY_UTILIZATION" +datasource "ds_sp_normalization" do + run_script $js_sp_normalization, $ds_sp_recommendations, $ds_applied_policy, $ds_vendor_account_table, $ds_currency, $ds_currency_conversion, $ds_conditional_currency_conversion, $param_min_savings, $param_days, $param_scope, $param_term, $param_payment_option, $param_savings_plan_type +end + +script "js_sp_normalization", type: "javascript" do + parameters "ds_sp_recommendations", "ds_applied_policy", "ds_vendor_account_table", "ds_currency", "ds_currency_conversion", "ds_conditional_currency_conversion", "param_min_savings", "param_days", "param_scope", "param_term", "param_payment_option", "param_savings_plan_type" + result "result" + code <<-'EOS' + // Used for formatting numbers to look pretty + function formatNumber(number, separator){ + numString = number.toString() + values = numString.split(".") + formatted_number = '' + + while (values[0].length > 3) { + var chunk = values[0].substr(-3) + values[0] = values[0].substr(0, values[0].length - 3) + formatted_number = separator + chunk + formatted_number } - var plan = { - "Compute Savings Plan":"COMPUTE_SP", - "EC2 Instance Savings Plan":"EC2_INSTANCE_SP", - "SageMaker Savings Plans":"SAGEMAKER_SP" + if (values[0].length > 0) { formatted_number = values[0] + formatted_number } + if (values[1] == undefined) { return formatted_number } + + return formatted_number + "." + values[1] + } + + service_table = { + "Compute Savings Plan": "Compute", + "EC2 Instance Savings Plan": "EC2", + "SageMaker Savings Plans": "SageMaker" + } + + result = [] + total_savings = 0.0 + + exchange_rate = ds_currency['exchange_rate'] + + _.each(ds_sp_recommendations, function(recommendation) { + resourceType = recommendation["instanceFamily"] + + if (resourceType == "" || resourceType == null) { + resourceType = param_savings_plan_type } - var payload = {} - payload['LookbackPeriodInDays'] = period[param_days] - payload['AccountScope'] = scope[param_scope] - payload['PaymentOption'] = option[param_payment_option] - payload['TermInYears'] = term[param_term] - payload['SavingsPlansType'] = plan[param_savings_plan_type] - var request = { - auth: "auth_aws", - pagination: "aws_pagination", - host: "ce.us-east-1.amazonaws.com", - path: '/', - verb: 'POST', - body_fields: payload, - headers: { - "User-Agent": "RS Policies", - "X-Amz-Target": "AWSInsightsIndexService.GetSavingsPlansPurchaseRecommendation", - "Content-Type": "application/x-amz-json-1.1", - } + currentAverageHourlyOnDemandSpend = recommendation["currentAverageHourlyOnDemandSpend"] * exchange_rate + currentMaximumHourlyOnDemandSpend = recommendation["currentMaximumHourlyOnDemandSpend"] * exchange_rate + currentMinimumHourlyOnDemandSpend = recommendation["currentMinimumHourlyOnDemandSpend"] * exchange_rate + estimatedAverageUtilization = recommendation["estimatedAverageUtilization"] * 1 + estimatedMonthlySavingsAmount = recommendation["estimatedMonthlySavingsAmount"] * exchange_rate + estimatedOnDemandCost = recommendation["estimatedOnDemandCost"] * exchange_rate + estimatedSavingsAmount = recommendation["estimatedSavingsAmount"] * exchange_rate + estimatedSavingsPercentage = recommendation["estimatedSavingsPercentage"] * 1 + estimatedSavingsPlanCost = recommendation["estimatedSavingsPlanCost"] * exchange_rate + upfrontCost = recommendation["upfrontCost"] * exchange_rate + + if (estimatedMonthlySavingsAmount >= param_min_savings) { + total_savings += recommendation["estimatedMonthlySavingsAmount"] * exchange_rate + + if (typeof(currentAverageHourlyOnDemandSpend) != 'number') { currentAverageHourlyOnDemandSpend = "" } + if (typeof(currentAverageHourlyOnDemandSpend) == 'number') { currentAverageHourlyOnDemandSpend = Number(currentAverageHourlyOnDemandSpend.toFixed(3)) } + + if (typeof(currentMaximumHourlyOnDemandSpend) != 'number') { currentMaximumHourlyOnDemandSpend = "" } + if (typeof(currentMaximumHourlyOnDemandSpend) == 'number') { currentMaximumHourlyOnDemandSpend = Number(currentMaximumHourlyOnDemandSpend.toFixed(3)) } + + if (typeof(currentMinimumHourlyOnDemandSpend) != 'number') { currentMinimumHourlyOnDemandSpend = "" } + if (typeof(currentMinimumHourlyOnDemandSpend) == 'number') { currentMinimumHourlyOnDemandSpend = Number(currentMinimumHourlyOnDemandSpend.toFixed(3)) } + + if (typeof(estimatedAverageUtilization) != 'number') { estimatedAverageUtilization = "" } + if (typeof(estimatedAverageUtilization) == 'number') { estimatedAverageUtilization = Number(estimatedAverageUtilization.toFixed(3)) } + + if (typeof(estimatedMonthlySavingsAmount) != 'number') { estimatedMonthlySavingsAmount = "" } + if (typeof(estimatedMonthlySavingsAmount) == 'number') { estimatedMonthlySavingsAmount = Number(estimatedMonthlySavingsAmount.toFixed(3)) } + + if (typeof(estimatedOnDemandCost) != 'number') { estimatedOnDemandCost = "" } + if (typeof(estimatedOnDemandCost) == 'number') { estimatedOnDemandCost = Number(estimatedOnDemandCost.toFixed(3)) } + + if (typeof(estimatedSavingsAmount) != 'number') { estimatedSavingsAmount = "" } + if (typeof(estimatedSavingsAmount) == 'number') { estimatedSavingsAmount = Number(estimatedSavingsAmount.toFixed(3)) } + + if (typeof(estimatedSavingsPercentage) != 'number') { estimatedSavingsPercentage = "" } + if (typeof(estimatedSavingsPercentage) == 'number') { estimatedSavingsPercentage = Number(estimatedSavingsPercentage.toFixed(2)) } + + if (typeof(estimatedSavingsPlanCost) != 'number') { estimatedSavingsPlanCost = "" } + if (typeof(estimatedSavingsPlanCost) == 'number') { estimatedSavingsPlanCost = Number(estimatedSavingsPlanCost.toFixed(3)) } + + if (typeof(upfrontCost) != 'number') { upfrontCost = "" } + if (typeof(upfrontCost) == 'number') { upfrontCost = Number(upfrontCost.toFixed(3)) } + + result.push({ + accountID: recommendation["accountID"], + accountName: ds_vendor_account_table[recommendation["accountID"]], + savingsCurrency: ds_currency['symbol'], + currentAverageHourlyOnDemandSpend: currentAverageHourlyOnDemandSpend, + currentMaximumHourlyOnDemandSpend: currentMaximumHourlyOnDemandSpend, + currentMinimumHourlyOnDemandSpend: currentMinimumHourlyOnDemandSpend, + estimatedAverageUtilization: estimatedAverageUtilization, + savings: estimatedMonthlySavingsAmount, + estimatedOnDemandCost: estimatedOnDemandCost, + estimatedSavingsAmount: estimatedSavingsAmount, + estimatedSavingsPercentage: estimatedSavingsPercentage, + estimatedSavingsPlanCost: estimatedSavingsPlanCost, + recommendedQuantity: recommendation["hourlyCommitmentToPurchase"], + instanceFamily: recommendation["instanceFamily"], + offeringId: recommendation["offeringId"], + region: recommendation["region"], + upfrontCost: upfrontCost, + term: param_term, + paymentOption: param_payment_option, + resourceType: resourceType, + lookbackPeriod: param_days, + service: service_table[param_savings_plan_type], + policy_name: ds_applied_policy['name'], + total_savings: "", + message: "" + }) } - EOS + }) + + // Messaging for currency conversion + currency = ds_currency['code'] + + if (ds_conditional_currency_conversion[0] != undefined) { + currency = ds_conditional_currency_conversion[0]['from'] + } + + conversion_message = "" + + if (ds_currency['code'] != currency && ds_currency_conversion.length > 0 && exchange_rate != 1) { + conversion_message = [ + "Savings values were converted from ", currency, " to ", ds_currency['code'], + " using an exchange rate of ", exchange_rate, ".\n\n" + ].join('') + } + + if (ds_currency['code'] != currency && ds_currency_conversion.length == 0) { + conversion_message = "Savings values are in ", currency, " due to a malfunction with Flexera's internal currency conversion API. Please contact Flexera support to report this issue." + } + + message = [ + "The following settings were used when generating recommendations:\n", + "- Savings Plan Type: ", param_savings_plan_type, "\n", + "- Account Scope: ", param_scope, "\n", + "- Term: ", param_term, "\n", + "- Look Back Period: ", param_days, "\n", + "- Payment Option: ", param_payment_option, "\n", + "- Minimum Savings Threshold: ", param_min_savings, "\n\n", + ].join('') + + disclaimer = "The above settings can be modified by editing the applied policy and changing the appropriate parameters.\n\n" + empty_fields = "Empty fields indicate information that is not applicable to that particular recommendation." + + savings_message = [ + ds_currency['symbol'], ' ', + formatNumber(parseFloat(total_savings).toFixed(2), ds_currency['t_separator']) + ].join('') + + // Sort by highest savings first + result = _.sortBy(result, function(item) { return item['savings'] * -1 }) + + if (result.length > 0) { + result[0]['total_savings'] = savings_message + result[0]['message'] = conversion_message + message + disclaimer + empty_fields + } +EOS end ############################################################################### # Policy ############################################################################## -policy "aws_sp_recommendations" do +policy "pol_aws_sp_recommendations" do validate_each $ds_sp_normalization do - summary_template "AWS Savings Plan Purchase Recommendations" - hash_include 'estimatedMonthlySavingsAmount' - escalate $email - check lt(to_n(val(item,"estimatedMonthlySavingsAmount")),$param_savings_threshold) - # AWS Savings Plan Purchase Recommendations - - ### Savings Plan Term: {{parameters.param_term}} - ### Savings Plan Type: {{parameters.param_savings_plan_type}} - ### Savings Plan Purchase Option: {{parameters.param_payment_option}} - ### Lookback Period: {{parameters.param_days}} + summary_template "{{ with index data 0 }}{{ .policy_name }}{{ end }}: {{ len data }} AWS Savings Plan Purchase Recommendations" + detail_template <<-'EOS' + **Potential Monthly Savings:** {{ with index data 0 }}{{ .total_savings }}{{ end }} + + {{ with index data 0 }}{{ .message }}{{ end }} + EOS + check lt(val(item, "savings"), 0) + escalate $esc_email export do - resource_level true + resource_level false field "accountID" do label "Account ID" end @@ -404,56 +579,53 @@ policy "aws_sp_recommendations" do field "region" do label "Region" end - field "estimatedMonthlySavingsAmountWithCurrencyCode" do + field "service" do + label "Service" + end + field "savingsCurrency" do + label "Currency" + end + field "savings" do label "Estimated Monthly Savings" end field "estimatedSavingsPercentage" do label "Estimated Savings Percentage" end - field "estimatedSavingsPlanCostWithCurrencyCode" do + field "estimatedSavingsPlanCost" do label "Estimated Savings Plan Cost" end + field "lookbackPeriod" do + label "Look Back Period (Days)" + end + field "paymentOption" do + label "Payment Option" + end field "recommendedQuantity" do - label "Quantity to Purchase" - path "hourlyCommitmentToPurchaseWithCurrencyCode" + label "Recommendeded Quantity to Purchase" end - field "upfrontCostWithCurrencyCode" do + field "upfrontCost" do label "Upfront Cost" end - field "instanceFamily" do - label "Instance Family" - end - field "currentAverageHourlyOnDemandSpendWithCurrencyCode" do - label "Current Avg Hourly OnDemand Spend" + field "currentAverageHourlyOnDemandSpend" do + label "Current Average Hourly On-Demand Spend" end - field "currentMaximumHourlyOnDemandSpendWithCurrencyCode" do - label "Current Max Hourly OnDemand Spend" + field "currentMaximumHourlyOnDemandSpend" do + label "Current Maximum Hourly On-Demand Spend" end - field "currentMinimumHourlyOnDemandSpendWithCurrencyCode" do - label "Current Min Hourly OnDemand Spend" + field "currentMinimumHourlyOnDemandSpend" do + label "Current Minimum Hourly On-Demand Spend" end field "offeringId" do label "Offering Id" end - field "id" do - label "Account Id" - path "accountID" - end - field "savings" do - label "Estimated Savings" - path "estimatedMonthlySavingsAmount" - end - field "term" do - label "Term" - end - field "paymentOption" do - label "Purchasing Option" + field "instanceFamily" do + label "Instance Family" end field "resourceType" do label "Resource Type" end - field "lookbackPeriod" do - label "Lookback Period" + field "term" do + label "Term" end end end @@ -463,7 +635,7 @@ end # Escalations ############################################################################### -escalation "email" do +escalation "esc_email" do automatic true label "Send Email" description "Send incident email"