diff --git a/cost/aws/reserved_instances/recommendations/CHANGELOG.md b/cost/aws/reserved_instances/recommendations/CHANGELOG.md index 8bbfaae2b7..27aa305f67 100644 --- a/cost/aws/reserved_instances/recommendations/CHANGELOG.md +++ b/cost/aws/reserved_instances/recommendations/CHANGELOG.md @@ -1,5 +1,17 @@ # 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 +- `ElasticSearch` is now referred to as `OpenSearch` in keeping with current AWS naming conventions +- 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.20 - Changed service metadata to "Compute" to ensure proper incident scraping diff --git a/cost/aws/reserved_instances/recommendations/README.md b/cost/aws/reserved_instances/recommendations/README.md index 90170e4e8f..057d4d0c70 100644 --- a/cost/aws/reserved_instances/recommendations/README.md +++ b/cost/aws/reserved_instances/recommendations/README.md @@ -1,24 +1,34 @@ -# AWS Reserved Instances Recommendation +# AWS Reserved Instances Recommendations -This Policy Template leverages the [AWS Reservation Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetReservationPurchaseRecommendation.html). It will raise incidents if AWS has any RI Purchase Recommendations, whose net savings exceeds the *Net Savings Threshold* parameter in the Policy. +## What it does -It will email the user specified in `Email addresses of the recipients you wish to notify` +This Policy Template reports any Reserved Instance Purchase Recommendations generated by AWS. The user can adjust which recommendations are reported via policy parameters. -> *NOTE: This Policy Template must be appled to the **AWS Organization Master Payer** account.* +> *NOTE: This Policy Template must be applied to the **AWS Organization Master Payer** account.* + +## Functional Details + +Recommendations are obtained via requests to the [AWS Reservation Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetReservationPurchaseRecommendation.html). + +### Policy Savings Details + +The policy includes the estimated savings. The estimated savings is recognized if the recommended reserved instance is purchased. The savings values are provided directly by the [AWS Reservation Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetReservationPurchaseRecommendation.html). + +If the Flexera organization is configured to use a currency other than the one the [AWS Reservation Purchase Recommendation API](https://docs.aws.amazon.com/aws-cost-management/latest/APIReference/API_GetReservationPurchaseRecommendation.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* - Number of days of prior usage to analyze -- *Service* - AWS Service for which to scan for RI Recommendations. Select All to include all services in a single incident and "All Except EC2" to include all services except the Elastic Compute Cloud (EC2). +- *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) -- *EC2 Specification* - EC2 RI type. Note: this parameter will be ignored if the Service parameter is not \"Elastic Compute Cloud (EC2)\" -- *RI Term* - Length of RI term -- *Payment Option* - RI purchase option. Leave blank to include all RI purchase options -- *Net Savings Threshold* - Specify the minimum estimated monthly savings that should result in a recommendation -- *Email addresses to notify* - Email addresses of the recipients you wish 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 +- *Service* - AWS Services to scan for recommendations. Items can be removed by clicking X to the right of the name. +- *EC2 Reservation Type* - The type of reservation recommendations to produce for EC2. Standard reservations are less flexible than convertible ones but provide a higher discount. Has no effect on recommendations for services other than `Elastic Compute Cloud (EC2)` +- *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. +- *Reservation Term* - Length of reservation term to provide recommendations for. +- *Payment Option* - Reservation purchase option to provide recommendations for. Select `Everything` to produce recommendations for all three. ## Policy Actions @@ -28,32 +38,32 @@ The following policy actions are taken on any resources found to be out of compl ## 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:GetReservationPurchaseRecommendation` -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:GetReservationPurchaseRecommendation" + ], + "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/reserved_instances/recommendations/aws_reserved_instance_recommendations.pt b/cost/aws/reserved_instances/recommendations/aws_reserved_instance_recommendations.pt index a8e5033d4d..66b761a88c 100644 --- a/cost/aws/reserved_instances/recommendations/aws_reserved_instance_recommendations.pt +++ b/cost/aws/reserved_instances/recommendations/aws_reserved_instance_recommendations.pt @@ -1,13 +1,13 @@ name "AWS Reserved Instances Recommendations" rs_pt_ver 20180301 type "policy" -short_description "A policy that sends email notifications when AWS RI Recommendations are identified. NOTE: These RI Purchase Recommendations are generated by AWS. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/aws/reserved_instances/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 Reserved Instance 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/reserved_instances/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.20", + version: "3.0", provider: "AWS", service: "Compute", policy_set: "Reserved Instances", @@ -18,94 +18,89 @@ info( # User Inputs ############################################################################### +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_aws_account_number" do + type "string" + category "Policy Settings" + label "Account Number" + description "The account number for AWS STS Cross Account Roles." + default "" +end + +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_days" do - category "RI" - 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" + category "Reservation 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_service" do - category "RI" + type "list" + category "Reservation Settings" label "Service" - default "All Except EC2" - description "AWS Service for which to scan for RI Recommendations" - allowed_values "All Except EC2","All","ElastiCache","Elastic Compute Cloud (EC2)","Elasticsearch Service (ES)","Redshift","Relational Database Service (RDS)" - type "string" + description "AWS Services to scan for recommendations. Items can be removed by clicking X to the right of the name." + default ["ElastiCache", "Elastic Compute Cloud (EC2)", "OpenSearch Service (ES)", "Redshift", "Relational Database Service (RDS)"] + allowed_values ["ElastiCache", "Elastic Compute Cloud (EC2)", "OpenSearch Service (ES)", "Redshift", "Relational Database Service (RDS)"] end parameter "param_service_spec" do - category "RI" - label "EC2 Specification" - allowed_values "Standard","Convertible" - default "Standard" - description "EC2 RI type. Note: this parameter will be ignored if the Service parameter is not \"Elastic Compute Cloud (EC2)\"" type "string" + category "Reservation Settings" + label "EC2 Reservation Type" + description "The type of reservation recommendations to produce for EC2. Standard reservations are less flexible than convertible ones but provide a higher discount." + default "Standard" + allowed_values "Standard", "Convertible" end parameter "param_scope" do - category "RI" + type "string" + category "Reservation 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" - type "string" + allowed_values "Payer", "Linked" end parameter "param_term" do - category "RI" - label "RI Term" - description "Length of RI term" - allowed_values "Any","1 Year","3 Year" - default "Any" type "string" -end - -parameter "param_aws_account_number" do - type "string" - label "Account Number" - description "The account number for AWS STS Cross Account Roles." - default "" + category "Reservation Settings" + label "Reservation Term" + description "Length of reservation term to provide recommendations for." + default "1 Year" + allowed_values "Any", "1 Year", "3 Year" end parameter "param_payment_option" do - category "RI" - label "(Optional) Payment Option" - default "" - allowed_values "","No Upfront","Partial Upfront","All Upfront","Light Utilization","Medium Utilization","Heavy Utilization" type "string" - description "RI purchase option. Leave blank to include all RI purchase options" -end - -parameter "param_savings_threshold" do - category "RI" - label "Net 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" -end - -parameter "param_currency_adjustment" do - label "Currency Adjustment" - description "adjusts recommendation savings based on USD conversion rate where USD is 100% - Maximum 5.0" - default 1.00 - max_value 5.00 - type "number" + category "Reservation Settings" + label "Payment Option" + description "Reservation purchase option to provide recommendations for. Select 'Everything' to produce recommendations for all three." + default "No Upfront" + allowed_values "No Upfront", "Partial Upfront", "All Upfront", "Everything" end ############################################################################### # Authentication ############################################################################### -#authenticate with AWS credentials "auth_aws" do schemes "aws","aws_sts" label "AWS" @@ -116,7 +111,7 @@ end credentials "auth_flexera" do schemes "oauth2" - label "flexera" + label "Flexera" description "Select FlexeraOne OAuth2 credentials" tags "provider=flexera" end @@ -125,7 +120,7 @@ end # Pagination ############################################################################### -pagination "aws_pagination" do +pagination "pagination_aws" do get_page_marker do body_path "NextPageToken" end @@ -135,64 +130,123 @@ 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] 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 + +# 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 -datasource "ds_account_name" do +# 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 "vendor_account_name", 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_ri_types" do run_script $js_ri_types, $param_service end @@ -201,308 +255,438 @@ script "js_ri_types", type: "javascript" do parameters "param_service" result "result" code <<-EOS - var result = [] - var services = ["ElastiCache","Elastic Compute Cloud (EC2)","Elasticsearch Service (ES)","Redshift","Relational Database Service (RDS)"] - if (param_service === "All"){ - _.each(services, function(service){ - result.push({"service":service}) - }) - } else if (param_service === "All Except EC2"){ - services.forEach(function(service) { - if (service !== "Elastic Compute Cloud (EC2)") { - result.push({"service":service}) - } - }); - } else { - result.push({"service":param_service}) - } - EOS + if (param_service.length == 0) { + services = [ + "ElastiCache", + "Elastic Compute Cloud (EC2)", + "Elasticsearch Service (ES)", + "Redshift", + "Relational Database Service (RDS)" + ] + } else { + services = [] + + _.each(param_service, function(service) { + if (service == "OpenSearch Service (ES)") { + services.push("Elasticsearch Service (ES)") + } else { + services.push(service) + } + }) + } + + result = _.map(services, function(service) { + return { service: service } + }) +EOS end datasource "ds_ri_recommendations" do iterate $ds_ri_types request do - run_script $js_ri_request, $param_days, $param_payment_option, val(iter_item,'service'), $param_service_spec, $param_term, $param_scope + run_script $js_ri_recommendations, val(iter_item, 'service'), $param_days, $param_payment_option, $param_service_spec, $param_term, $param_scope end result do encoding "json" collect jmes_path(response, "Recommendations[*]") do - field "accountScope", jmes_path(col_item,"AccountScope") - field "lookbackPeriodInDays", jmes_path(col_item,"LookbackPeriodInDays") - field "paymentOption", jmes_path(col_item,"PaymentOption") - field "termInYears", jmes_path(col_item,"TermInYears") - field "service", val(iter_item,"service") + field "accountScope", jmes_path(col_item, "AccountScope") + field "lookbackPeriodInDays", jmes_path(col_item, "LookbackPeriodInDays") + field "paymentOption", jmes_path(col_item, "PaymentOption") + field "termInYears", jmes_path(col_item, "TermInYears") + field "service", val(iter_item, "service") field "recommendationDetails" do collect jmes_path(col_item, "RecommendationDetails[*]") do - field "accountID", jmes_path(col_item,"AccountId") - field "averageNormalizedUnitsUsedPerHour", jmes_path(col_item,"AverageNormalizedUnitsUsedPerHour") - field "averageNumberOfInstancesUsedPerHour", jmes_path(col_item,"AverageNumberOfInstancesUsedPerHour") - field "averageUtilization", jmes_path(col_item,"AverageUtilization") - field "estimatedBreakEvenInMonths", jmes_path(col_item,"EstimatedBreakEvenInMonths") - field "estimatedMonthlyOnDemandCost", jmes_path(col_item,"EstimatedMonthlyOnDemandCost") - field "estimatedMonthlySavingsAmount", jmes_path(col_item,"EstimatedMonthlySavingsAmount") - field "estimatedMonthlySavingsPercentage", jmes_path(col_item,"EstimatedMonthlySavingsPercentage") - field "estimatedReservationCostForLookbackPeriod", jmes_path(col_item,"EstimatedReservationCostForLookbackPeriod") - field "availabilityZone", jmes_path(col_item,"InstanceDetails.*.AvailabilityZone") - field "currentGeneration", jmes_path(col_item,"InstanceDetails.*.CurrentGeneration") - field "family", jmes_path(col_item,"InstanceDetails.*.Family") - field "instanceType", jmes_path(col_item,"InstanceDetails.*.InstanceType") - field "platform", jmes_path(col_item,"InstanceDetails.*.Platform") - field "region", jmes_path(col_item,"InstanceDetails.*.Region") - field "sizeFlexEligible", jmes_path(col_item,"InstanceDetails.*.SizeFlexEligible") - field "tenancy", jmes_path(col_item,"InstanceDetails.*.Tenancy") - field "nodeType", jmes_path(col_item,"InstanceDetails.*.NodeType") - field "productDescription", jmes_path(col_item,"InstanceDetails.*.ProductDescription") - field "instanceClass", jmes_path(col_item,"InstanceDetails.*.InstanceClass") - field "instanceSize", jmes_path(col_item,"InstanceDetails.*.InstanceSize") - field "databaseEngine", jmes_path(col_item,"InstanceDetails.*.DatabaseEngine") - field "databaseEdition", jmes_path(col_item,"InstanceDetails.*.DatabaseEdition") - field "deploymentOption", jmes_path(col_item,"InstanceDetails.*.DeploymentOption") - field "licenseModel", jmes_path(col_item,"InstanceDetails.*.LicenseModel") - field "maximumNormalizedUnitsUsedPerHour", jmes_path(col_item,"MaximumNormalizedUnitsUsedPerHour") - field "maximumNumberOfInstancesUsedPerHour", jmes_path(col_item,"MaximumNumberOfInstancesUsedPerHour") - field "minimumNormalizedUnitsUsedPerHour", jmes_path(col_item,"MinimumNormalizedUnitsUsedPerHour") - field "minimumNumberOfInstancesUsedPerHour", jmes_path(col_item,"MinimumNumberOfInstancesUsedPerHour") - field "recommendedNormalizedUnitsToPurchase", jmes_path(col_item,"RecommendedNormalizedUnitsToPurchase") - field "recommendedNumberOfInstancesToPurchase", jmes_path(col_item,"RecommendedNumberOfInstancesToPurchase") - field "recurringStandardMonthlyCost", jmes_path(col_item,"RecurringStandardMonthlyCost") - field "upfrontCost", jmes_path(col_item,"UpfrontCost") + field "accountID", jmes_path(col_item, "AccountId") + field "averageNormalizedUnitsUsedPerHour", jmes_path(col_item, "AverageNormalizedUnitsUsedPerHour") + field "averageNumberOfInstancesUsedPerHour", jmes_path(col_item, "AverageNumberOfInstancesUsedPerHour") + field "averageUtilization", jmes_path(col_item, "AverageUtilization") + field "currency", jmes_path(col_item, "CurrencyCode") + field "estimatedBreakEvenInMonths", jmes_path(col_item, "EstimatedBreakEvenInMonths") + field "estimatedMonthlyOnDemandCost", jmes_path(col_item, "EstimatedMonthlyOnDemandCost") + field "estimatedMonthlySavingsAmount", jmes_path(col_item, "EstimatedMonthlySavingsAmount") + field "estimatedMonthlySavingsPercentage", jmes_path(col_item, "EstimatedMonthlySavingsPercentage") + field "estimatedReservationCostForLookbackPeriod", jmes_path(col_item, "EstimatedReservationCostForLookbackPeriod") + field "availabilityZone", jmes_path(col_item, "InstanceDetails.*.AvailabilityZone") + field "currentGeneration", jmes_path(col_item, "InstanceDetails.*.CurrentGeneration") + field "family", jmes_path(col_item, "InstanceDetails.*.Family") + field "instanceType", jmes_path(col_item, "InstanceDetails.*.InstanceType") + field "platform", jmes_path(col_item, "InstanceDetails.*.Platform") + field "region", jmes_path(col_item, "InstanceDetails.*.Region") + field "sizeFlexEligible", jmes_path(col_item, "InstanceDetails.*.SizeFlexEligible") + field "tenancy", jmes_path(col_item, "InstanceDetails.*.Tenancy") + field "nodeType", jmes_path(col_item, "InstanceDetails.*.NodeType") + field "productDescription", jmes_path(col_item, "InstanceDetails.*.ProductDescription") + field "instanceClass", jmes_path(col_item, "InstanceDetails.*.InstanceClass") + field "instanceSize", jmes_path(col_item, "InstanceDetails.*.InstanceSize") + field "databaseEngine", jmes_path(col_item, "InstanceDetails.*.DatabaseEngine") + field "databaseEdition", jmes_path(col_item, "InstanceDetails.*.DatabaseEdition") + field "deploymentOption", jmes_path(col_item, "InstanceDetails.*.DeploymentOption") + field "licenseModel", jmes_path(col_item, "InstanceDetails.*.LicenseModel") + field "maximumNormalizedUnitsUsedPerHour", jmes_path(col_item, "MaximumNormalizedUnitsUsedPerHour") + field "maximumNumberOfInstancesUsedPerHour", jmes_path(col_item, "MaximumNumberOfInstancesUsedPerHour") + field "minimumNormalizedUnitsUsedPerHour", jmes_path(col_item, "MinimumNormalizedUnitsUsedPerHour") + field "minimumNumberOfInstancesUsedPerHour", jmes_path(col_item, "MinimumNumberOfInstancesUsedPerHour") + field "recommendedNormalizedUnitsToPurchase", jmes_path(col_item, "RecommendedNormalizedUnitsToPurchase") + field "recommendedNumberOfInstancesToPurchase", jmes_path(col_item, "RecommendedNumberOfInstancesToPurchase") + field "recurringStandardMonthlyCost", jmes_path(col_item, "RecurringStandardMonthlyCost") + field "upfrontCost", jmes_path(col_item, "UpfrontCost") end end end end end -datasource "ds_ri_normalization" do - run_script $js_ri_cleanup, $ds_ri_recommendations, $param_currency_adjustment, $ds_account_name -end - -############################################################################### -# Scripts -############################################################################### - -script "js_get_account_name", type: "javascript" do - parameters "account_id", "billing_centers", "org", "optima_host" +script "js_ri_recommendations", type: "javascript" do + parameters "service", "param_days", "param_payment_option", "param_service_spec", "param_term", "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; + // 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" + } + + service_table = { + "Elastic Compute Cloud (EC2)": "Amazon Elastic Compute Cloud - Compute", + "Relational Database Service (RDS)": "Amazon Relational Database Service", + "ElastiCache": "Amazon ElastiCache", + "Redshift": "Amazon Redshift", + "Elasticsearch Service (ES)": "Amazon OpenSearch Service" + } + + term_table = { + "1 Year": "ONE_YEAR", + "3 Year": "THREE_YEARS" + } + + // Build out the body of the request based on parameters + body_fields = { + LookbackPeriodInDays: period_table[param_days], + AccountScope: param_scope.toUpperCase(), + Service: service_table[service] + } + + if (param_payment_option != "Everything") { + body_fields['PaymentOption'] = param_payment_option.replace(' ', '_').toUpperCase() + } + + if (service == "Elastic Compute Cloud (EC2)") { + body_fields['ServiceSpecification'] = { + EC2Specification: { OfferingClass: param_service_spec.toUpperCase() } } - 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] + } + + if (param_term != "Any") { + body_fields['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.GetReservationPurchaseRecommendation", + "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_ri_recommendations end -script "js_ri_cleanup", type: "javascript" do - parameters "ri_recos", "currency_adjustment", "ds_account_name" +script "js_conditional_currency_conversion", type: "javascript" do + parameters "ds_currency_target", "ds_ri_recommendations" result "result" code <<-EOS - var result = []; - accountName = "" - if (ds_account_name[0] != null) { - accountName = ds_account_name[0]['vendor_account_name'] - } - _.each(ri_recos, function(recos){ - var reco_details = recos["recommendationDetails"]; - _.each(reco_details, function(reco){ - var term = recos["termInYears"].toString() + " year" - if (recos["termInYears"] != 1) { - term += "s" - } - result.push({ - accountID: reco["accountID"], - accountName: accountName, - service: recos["service"], - accountScope: recos["accountScope"], - lookbackPeriodInDays: recos["lookbackPeriodInDays"], - paymentOption: recos["paymentOption"], - availabilityZone: reco["availabilityZone"].toString(), - averageNormalizedUnitsUsedPerHour: (Math.round(reco["averageNormalizedUnitsUsedPerHour"] * 100) / 100).toString(10), - averageNumberOfInstancesUsedPerHour: (Math.round(reco["averageNumberOfInstancesUsedPerHour"] * 100) / 100).toString(10), - averageUtilization: (Math.round(reco["averageUtilization"] * 100) / 100).toString(10), - databaseEdition: reco["databaseEdition"].toString(), - databaseEngine: reco["databaseEngine"].toString(), - deploymentOption: reco["deploymentOption"].toString(), - estimatedBreakEvenInMonths: (Math.round(reco["estimatedBreakEvenInMonths"] * 100) / 100).toString(10), - estimatedMonthlyOnDemandCost: (Math.round(reco["estimatedMonthlyOnDemandCost"] * 100) / 100).toString(10), - estimatedMonthlySavingsAmount: (Math.round(reco["estimatedMonthlySavingsAmount"] * 100) / 100 * currency_adjustment).toString(10), - estimatedMonthlySavingsPercentage: (Math.round(reco["estimatedMonthlySavingsPercentage"] * 100) / 100).toString(10), - estimatedReservationCostForLookbackPeriod: (Math.round(reco["estimatedReservationCostForLookbackPeriod"] * 100) / 100).toString(10), - family: reco["family"].toString(), - instanceClass: reco["instanceClass"].toString(), - instanceSize: reco["instanceSize"].toString(), - instanceType: reco["instanceType"].toString(), - licenseModel: reco["licenseModel"].toString(), - maximumNormalizedUnitsUsedPerHour: (Math.round(reco["maximumNormalizedUnitsUsedPerHour"] * 100) / 100).toString(10), - maximumNumberOfInstancesUsedPerHour: (Math.round(reco["maximumNumberOfInstancesUsedPerHour"] * 100) / 100).toString(10), - minimumNormalizedUnitsUsedPerHour: (Math.round(reco["minimumNormalizedUnitsUsedPerHour"] * 100) / 100).toString(10), - minimumNumberOfInstancesUsedPerHour: (Math.round(reco["minimumNumberOfInstancesUsedPerHour"] * 100) / 100).toString(10), - nodeType: reco["nodeType"].toString(), - platform: reco["platform"].toString() || reco["databaseEngine"].toString(), - productDescription: reco["productDescription"].toString(), - recommendedNormalizedUnitsToPurchase: reco["recommendedNormalizedUnitsToPurchase"].toString(), - recommendedNumberOfInstancesToPurchase: reco["recommendedNumberOfInstancesToPurchase"].toString(), - recurringStandardMonthlyCost: (Math.round(reco["recurringStandardMonthlyCost"] * 100) / 100).toString(10), - region: reco["region"].toString(), - sizeFlexEligible: reco["sizeFlexEligible"].toString(), - tenancy: reco["tenancy"].toString(), - upfrontCost: (Math.round(reco["upfrontCost"] * 100) / 100).toString(10), - resourceType: reco["instanceType"].toString() || reco["instanceSize"].toString() || reco["nodeType"].toString(), - term: term - }) - }) - }) - EOS -end + result = [] + from_currency = "USD" -script "js_ri_request", type: "javascript" do - parameters "param_days","param_payment_option","param_service","param_service_spec","param_term","param_scope" - result "request" - code <<-EOS - - var period = { - "Last 7 Days":"SEVEN_DAYS", - "Last 30 Days":"THIRTY_DAYS", - "Last 60 Days":"SIXTY_DAYS" + if (ds_ri_recommendations.length > 0) { + if (ds_ri_recommendations[0]['recommendationDetails'].length > 0) { + if (typeof(ds_ri_recommendations[0]['recommendationDetails'][0]['currency']) == 'string') { + from_currency = ds_ri_recommendations[0]['recommendationDetails'][0]['currency'] + } } + } - var service = { - "Elastic Compute Cloud (EC2)":"Amazon Elastic Compute Cloud - Compute", - "Relational Database Service (RDS)":"Amazon Relational Database Service", - "ElastiCache":"Amazon ElastiCache", - "Redshift":"Amazon Redshift", - "Elasticsearch Service (ES)":"Amazon Elasticsearch Service" - } + // Make the request only if the target currency is not USD + if (ds_currency_target['code'] != from_currency) { + result = [{ from: from_currency }] + } +EOS +end - var service_spec = { - "Standard":"STANDARD", - "Convertible":"CONVERTIBLE" - } +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 - var term = { - "1 Year":"ONE_YEAR", - "3 Year":"THREE_YEARS" - } +datasource "ds_currency" do + run_script $js_currency, $ds_currency_target, $ds_currency_conversion +end - var scope = { - "Payer":"PAYER", - "Linked":"LINKED" - } +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 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" - } + if (ds_currency_conversion['to'] != undefined) { + currency_code = ds_currency_target['code'] + current_month = parseInt(new Date().toISOString().split('-')[1]) - var payload = {} - payload['LookbackPeriodInDays'] = period[param_days] - payload['AccountScope'] = scope[param_scope] - payload['Service'] = service[param_service] + conversion_block = _.find(ds_currency_conversion['to'][currency_code], function(item) { + return item['month'] == current_month + }) - if(param_payment_option !== ""){ - payload['PaymentOption'] = option[param_payment_option] + if (conversion_block != undefined) { + result['exchange_rate'] = conversion_block['monthlyAverage'] } + } +EOS +end - if(param_service === "Elastic Compute Cloud (EC2)"){ - payload['ServiceSpecification'] = {} - payload['ServiceSpecification']['EC2Specification'] = {} - payload['ServiceSpecification']['EC2Specification']['OfferingClass'] = service_spec[param_service_spec] - } +datasource "ds_ri_normalization" do + run_script $js_ri_normalization, $ds_ri_recommendations, $ds_applied_policy, $ds_vendor_account_table, $ds_currency, $ds_currency_conversion, $ds_conditional_currency_conversion, $param_days, $param_service, $param_service_spec, $param_scope, $param_term, $param_payment_option, $param_min_savings +end - if(param_term !== "Any"){ - payload['TermInYears'] = term[param_term] +script "js_ri_normalization", type: "javascript" do + parameters "ds_ri_recommendations", "ds_applied_policy", "ds_vendor_account_table", "ds_currency", "ds_currency_conversion", "ds_conditional_currency_conversion", "param_days", "param_service", "param_service_spec", "param_scope", "param_term", "param_payment_option", "param_min_savings" + 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 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.GetReservationPurchaseRecommendation", - "Content-Type": "application/x-amz-json-1.1", + if (values[0].length > 0) { formatted_number = values[0] + formatted_number } + if (values[1] == undefined) { return formatted_number } + + return formatted_number + "." + values[1] + } + + result = [] + total_savings = 0.0 + + exchange_rate = ds_currency['exchange_rate'] + + _.each(ds_ri_recommendations, function(item) { + term = item["termInYears"].toString() + " year" + if (item["termInYears"] > 1) { term += "s" } + + _.each(item["recommendationDetails"], function(recommendation) { + if (recommendation["estimatedMonthlySavingsAmount"] >= param_min_savings) { + total_savings += recommendation["estimatedMonthlySavingsAmount"] * exchange_rate + + // Set blank values for fields where the value is N/A for the recommendation in question + averageNumberOfInstancesUsedPerHour = Math.round(recommendation["averageNumberOfInstancesUsedPerHour"] * 100) / 100 + maximumNumberOfInstancesUsedPerHour = Math.round(recommendation["maximumNumberOfInstancesUsedPerHour"] * 100) / 100 + minimumNumberOfInstancesUsedPerHour = Math.round(recommendation["minimumNumberOfInstancesUsedPerHour"] * 100) / 100 + averageNormalizedUnitsUsedPerHour = Math.round(recommendation["averageNormalizedUnitsUsedPerHour"] * 100) / 100 + maximumNormalizedUnitsUsedPerHour = Math.round(recommendation["maximumNormalizedUnitsUsedPerHour"] * 100) / 100 + minimumNormalizedUnitsUsedPerHour = Math.round(recommendation["minimumNormalizedUnitsUsedPerHour"] * 100) / 100 + recommendedNormalizedUnitsToPurchase = parseFloat(recommendation["recommendedNormalizedUnitsToPurchase"]) + recommendedNumberOfInstancesToPurchase = parseFloat(recommendation["recommendedNumberOfInstancesToPurchase"]) + + if (isNaN(averageNumberOfInstancesUsedPerHour)) { averageNumberOfInstancesUsedPerHour = "" } + if (isNaN(maximumNumberOfInstancesUsedPerHour)) { maximumNumberOfInstancesUsedPerHour = "" } + if (isNaN(minimumNumberOfInstancesUsedPerHour)) { minimumNumberOfInstancesUsedPerHour = "" } + if (isNaN(averageNormalizedUnitsUsedPerHour)) { averageNormalizedUnitsUsedPerHour = "" } + if (isNaN(maximumNormalizedUnitsUsedPerHour)) { maximumNormalizedUnitsUsedPerHour = "" } + if (isNaN(minimumNormalizedUnitsUsedPerHour)) { minimumNormalizedUnitsUsedPerHour = "" } + if (isNaN(recommendedNormalizedUnitsToPurchase)) { recommendedNormalizedUnitsToPurchase = "" } + if (isNaN(recommendedNumberOfInstancesToPurchase)) { recommendedNumberOfInstancesToPurchase = "" } + + // Calculate values while including currency calculation when needed + averageUtilization = Math.round(recommendation["averageUtilization"] * 100) / 100 + estimatedBreakEvenInMonths = Math.round(recommendation["estimatedBreakEvenInMonths"] * exchange_rate * 100) / 100 + estimatedMonthlyOnDemandCost = Math.round(recommendation["estimatedMonthlyOnDemandCost"] * exchange_rate * 100) / 100 + estimatedMonthlySavingsAmount = Math.round(recommendation["estimatedMonthlySavingsAmount"] * exchange_rate * 100) / 100 + estimatedMonthlySavingsPercentage = Math.round(recommendation["estimatedMonthlySavingsPercentage"] * 100) / 100 + estimatedReservationCostForLookbackPeriod = Math.round(recommendation["estimatedReservationCostForLookbackPeriod"] * exchange_rate) / 100 + recurringStandardMonthlyCost = Math.round(recommendation["recurringStandardMonthlyCost"] * exchange_rate * 100) / 100 + upfrontCost = Math.round(recommendation["upfrontCost"] * exchange_rate * 100) / 100 + + // Set misc values + platform = recommendation["platform"].toString() || recommendation["databaseEngine"].toString() + resourceType = recommendation["instanceType"].toString() || recommendation["instanceSize"].toString() || recommendation["nodeType"].toString() + paymentOption = item["paymentOption"].replace('_', ' ') + + result.push({ + accountID: recommendation["accountID"], + accountName: ds_vendor_account_table[recommendation["accountID"]], + service: item["service"], + scope: item["accountScope"], + lookbackPeriod: item["lookbackPeriodInDays"], + paymentOption: paymentOption, + availabilityZone: recommendation["availabilityZone"].toString(), + averageNormalizedUnitsUsedPerHour: averageNormalizedUnitsUsedPerHour, + averageNumberOfInstancesUsedPerHo: averageNumberOfInstancesUsedPerHour, + averageUtilization: averageUtilization, + databaseEdition: recommendation["databaseEdition"].toString(), + databaseEngine: recommendation["databaseEngine"].toString(), + deploymentOption: recommendation["deploymentOption"].toString(), + estimatedBreakEvenInMonths: estimatedBreakEvenInMonths, + estimatedMonthlyOnDemandCost: estimatedMonthlyOnDemandCost, + savings: estimatedMonthlySavingsAmount, + savingsCurrency: ds_currency['symbol'], + estimatedMonthlySavingsPercentage: estimatedMonthlySavingsPercentage, + estimatedReservationCostForLookbackPeriod: estimatedReservationCostForLookbackPeriod, + family: recommendation["family"].toString(), + instanceClass: recommendation["instanceClass"].toString(), + instanceSize: recommendation["instanceSize"].toString(), + instanceType: recommendation["instanceType"].toString(), + licenseModel: recommendation["licenseModel"].toString(), + maximumNormalizedUnitsUsedPerHour: maximumNormalizedUnitsUsedPerHour, + maximumNumberOfInstancesUsedPerHour: maximumNumberOfInstancesUsedPerHour, + minimumNormalizedUnitsUsedPerHour: minimumNormalizedUnitsUsedPerHour, + minimumNumberOfInstancesUsedPerHour: minimumNumberOfInstancesUsedPerHour, + nodeType: recommendation["nodeType"].toString(), + platform: platform, + productDescription: recommendation["productDescription"].toString(), + recommendedNormalizedUnitsToPurchase: recommendedNormalizedUnitsToPurchase, + recommendedQuantity: recommendedNumberOfInstancesToPurchase, + recurringStandardMonthlyCost: recurringStandardMonthlyCost, + region: recommendation["region"].toString(), + sizeFlexEligible: recommendation["sizeFlexEligible"].toString(), + tenancy: recommendation["tenancy"].toString(), + upfrontCost: upfrontCost, + resourceType: resourceType, + term: term, + savingsCurrency: ds_currency['symbol'], + 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['to'] != undefined && 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['to'] == undefined) { + 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." + } + + // Only show EC2 Specification setting if it is applicable + ec2_spec = "" + if (param_service.length == 0 || _.contains(param_service, "Elastic Compute Cloud (EC2)")) { + ec2_spec = "- EC2 Specification: " + param_service_spec + "\n" + } + + message = [ + "The following settings were used when generating recommendations:\n", + "- AWS Services: ", param_service.join(', '), "\n", + ec2_spec, + "- 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_ri_recommendations" do +policy "pol_aws_ri_recommendations" do validate_each $ds_ri_normalization do - summary_template "{{ len data }} AWS Reserved Instances Purchase Recommendations" - hash_include 'estimatedMonthlySavingsAmount' - escalate $email - check lt(to_n(val(item,"estimatedMonthlySavingsAmount")),$param_savings_threshold) + summary_template "{{ with index data 0 }}{{ .policy_name }}{{ end }}: {{ len data }} AWS Reserved Instances 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 false + field "accountID" do + label "Account ID" + end + field "accountName" do + label "Account Name" + end field "region" do label "Region" end field "service" do label "Service" end - field "accountID" do - label "Account ID" - end - field "accountName" do - label "Account Name" + field "savingsCurrency" do + label "Currency" end field "savings" do label "Estimated Monthly Savings Amount" - path "estimatedMonthlySavingsAmount" end field "scope" do label "Account Scope" - path "accountScope" end field "averageNormalizedUnitsUsedPerHour" do label "Average Normalized Units Used Per Hour" @@ -550,8 +734,7 @@ policy "aws_ri_recommendations" do label "License Model" end field "lookbackPeriod" do - label "Look Back Period In Days" - path "lookbackPeriodInDays" + label "Look Back Period (Days)" end field "maximumNormalizedUnitsUsedPerHour" do label "Maximum Normalized Units Used Per Hour" @@ -582,7 +765,6 @@ policy "aws_ri_recommendations" do end field "recommendedQuantity" do label "Recommended Number Of Instances To Purchase" - path "recommendedNumberOfInstancesToPurchase" end field "recurringStandardMonthlyCost" do label "Recurring Standard Monthly Cost" @@ -610,7 +792,7 @@ end # Escalations ############################################################################### -escalation "email" do +escalation "esc_email" do automatic true label "Send Email" description "Send incident email"