diff --git a/cost/aws/reserved_instances/expiration/CHANGELOG.md b/cost/aws/reserved_instances/expiration/CHANGELOG.md index b33f78162b..a082fa8555 100644 --- a/cost/aws/reserved_instances/expiration/CHANGELOG.md +++ b/cost/aws/reserved_instances/expiration/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v3.0.0 + +- Policy is no longer deprecated +- Added ability to filter results by Billing Center +- Additional fields added to incident to provide more context +- Streamlined code for better readability and faster execution + ## v2.2.1 - Added `deprecated` field to policy metadata. Functionality is unchanged. diff --git a/cost/aws/reserved_instances/expiration/README.md b/cost/aws/reserved_instances/expiration/README.md index 08cf3ef229..31ab609a29 100644 --- a/cost/aws/reserved_instances/expiration/README.md +++ b/cost/aws/reserved_instances/expiration/README.md @@ -1,41 +1,33 @@ # AWS Expiring Reserved Instances -## Deprecated +## What It Does -This policy is no longer being updated. +This policy template produces a report of all AWS reservations that have expired or are going to expire within a user-specified number of days. Optionally, this report can be emailed. -## What it does +## Input Parameters + +- *Email Addresses* - Email addresses of the recipients you wish to notify when new incidents are created. +- *Days Until Expiration* - The number of days until expiration to include a Reservation in the report. Set to `0` to only report expired Reservations. +- *Allow/Deny Billing Centers* - Allow or Deny entered Billing Centers. +- *Allow/Deny Billing Center List* - A list of allowed or denied Billing Center names/IDs. Leave blank to report on Reservations in all Billing Centers. -This Policy Template leverages the Optima Bill Data for AWS Reserved Instances. It will notify only if expiration is within the timeframe specified in `Number of days to prior to expiration date to trigger incident` field. It will email the user specified in `Email addresses of the recipients you wish to notify`. +## Policy Actions + +- Send an email report ## Prerequisites 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). - [**Flexera Credential**](https://docs.flexera.com/flexera/EN/Automation/ProviderCredentials.htm) (*provider=flexera*) which has the following roles: - - `actor` - - `observer` - - `credential_viewer` + - `billing_center_viewer` 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. -## Input Parameters - -This policy has the following input parameters required when launching the policy. - -- *Number of days to prior to expiration date to trigger incident* - enter the number of days you want before the Reserved Instance expires. -- *Email addresses of the recipients you wish to notify* - A list of email addresses to notify - -## Policy Actions - -The following policy actions are taken on any resources found to be out of compliance. - -- Send an email report - ## Supported Clouds - AWS ## Cost -This Policy Template does not launch any instances, and so does not incur any cloud costs. +This Policy Template does not incur any cloud costs. diff --git a/cost/aws/reserved_instances/expiration/aws_reserved_instance_expiration.pt b/cost/aws/reserved_instances/expiration/aws_reserved_instance_expiration.pt new file mode 100644 index 0000000000..693f00c399 --- /dev/null +++ b/cost/aws/reserved_instances/expiration/aws_reserved_instance_expiration.pt @@ -0,0 +1,338 @@ +name "AWS Expiring Reserved Instances" +rs_pt_ver 20180301 +type "policy" +short_description "Reports on AWS Reserved Instances that have or will soon expire. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/aws/reserved_instances/expiration) and [docs.flexera.com/flexera/EN/Automation](https://docs.flexera.com/flexera/EN/Automation/AutomationGS.htm) to learn more." +long_description "" +category "Cost" +severity "medium" +default_frequency "daily" +info( + version: "3.0.0", + provider: "AWS", + service: "Compute", + policy_set: "Reserved Instances" +) + +############################################################################### +# Parameters +############################################################################### + +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_days_expiration" do + type "number" + category "Policy Settings" + label "Days Until Expiration" + description "The number of days until expiration to include a Reservation in the report. Set to '0' to only report expired Reservations." + min_value 0 + default 15 +end + +parameter "param_bc_allow_or_deny" do + type "string" + category "Filters" + label "Allow/Deny Billing Centers" + description "Allow or Deny entered Billing Centers." + allowed_values "Allow", "Deny" + default "Allow" +end + +parameter "param_billing_centers" do + type "list" + category "Filters" + label "Allow/Deny Billing Center List" + description "A list of allowed or denied Billing Center names/IDs. Leave blank to report on Reservations in all Billing Centers." + default [] +end + +############################################################################### +# Authentication +############################################################################### + +credentials "auth_flexera" do + schemes "oauth2" + label "Flexera" + description "Select Flexera One OAuth2 credentials" + tags "provider=flexera" +end + +############################################################################### +# Datasources & Scripts +############################################################################### + +# Get applied policy metadata for use later +datasource "ds_applied_policy" do + request do + 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 +end + +datasource "ds_billing_centers" do + request do + auth $auth_flexera + host rs_optima_host + path join(["/analytics/orgs/", rs_org_id, "/billing_centers"]) + header "Api-Version", "1.0" + header "User-Agent", "RS Policies" + 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 + end +end + +datasource "ds_billing_centers_filtered" do + run_script $js_billing_centers_filtered, $ds_billing_centers, $param_bc_allow_or_deny, $param_billing_centers +end + +script "js_billing_centers_filtered", type: "javascript" do + parameters "ds_billing_centers", "param_bc_allow_or_deny", "param_billing_centers" + result "result" + code <<-EOS + allow_deny_test = { "Allow": true, "Deny": false } + + if (param_billing_centers.length > 0) { + billing_centers = _.filter(ds_billing_centers, function(item) { + id_found = _.contains(param_billing_centers, item['id']) == allow_deny_test[param_bc_allow_or_deny] + name_found = _.contains(param_billing_centers, item['name']) == allow_deny_test[param_bc_allow_or_deny] + return id_found || name_found + }) + + // Check for conflicting parents/children and remove children if present + bc_ids = _.compact(_.pluck(billing_centers, 'id')) + bad_children = _.filter(ds_billing_centers, function(bc) { return _.contains(bc_ids, bc['parent_id']) }) + bad_children_ids = _.pluck(bad_children, 'id') + + // Create final result with the bad children removed + final_list = _.reject(billing_centers, function(bc) { return _.contains(bad_children_ids, bc['id']) }) + } else { + // If we're not filtering at all, just grab all of the top level billing centers + final_list = _.filter(ds_billing_centers, function(bc) { + return bc['parent_id'] == null || bc['parent_id'] == undefined + }) + } + + result = _.compact(_.pluck(final_list, 'id')) +EOS +end + +datasource "ds_accounts_by_billing_center" do + request do + run_script $js_accounts_by_billing_center, $ds_billing_centers_filtered, rs_org_id, rs_optima_host + end + result do + encoding "json" + collect jmes_path(response, "rows[*]") do + field "cost", jmes_path(col_item, "metrics.cost_amortized_unblended_adj") + field "billing_center_id", jmes_path(col_item, "dimensions.billing_center_id") + field "vendor_account_id", jmes_path(col_item, "dimensions.vendor_account") + field "vendor_account_name", jmes_path(col_item, "dimensions.vendor_account_name") + field "timestamp", jmes_path(col_item, "timestamp") + end + end +end + +script "js_accounts_by_billing_center", type: "javascript" do + parameters "ds_billing_centers_filtered", "rs_org_id", "rs_optima_host" + result "request" + code <<-EOS + start_date = new Date().toISOString() + start_date = start_date.split('-')[0] + '-' + start_date.split('-')[1] + + end_year = Number(start_date.split('-')[0]) + end_month = Number(start_date.split('-')[1]) + 1 + if (end_month == 13) { end_month = 1; end_year++ } + if (end_month < 10) { end_month = '0' + end_month.toString() } + end_date = end_year.toString() + '-' + end_month.toString() + + var request = { + auth: "auth_flexera", + verb: "POST", + host: rs_optima_host, + path: "/bill-analysis/orgs/" + rs_org_id + "/costs/aggregated", + body_fields: { + "dimensions": [ "billing_center_id", "vendor_account", "vendor_account_name" ], + "granularity": "month", + "metrics": [ "cost_amortized_unblended_adj" ], + "billing_center_ids": ds_billing_centers_filtered, + "start_at": start_date, + "end_at": end_date + } + } +EOS +end + +datasource "ds_aws_reservations" do + request do + auth $auth_flexera + host rs_optima_host + path join(["/reserved_instances/orgs/", rs_org_id, "/clouds/aws"]) + end + result do + encoding "json" + collect jmes_path(response, "[*]") do + field "end_date", jmes_path(col_item, "end_datetime") + field "start_date", jmes_path(col_item, "start_datetime") + field "account_name", jmes_path(col_item, "account_name") + field "account_id", jmes_path(col_item, "account_id") + field "lease_id", jmes_path(col_item, "lease_id") + field "region", jmes_path(col_item, "region") + field "instance_type", jmes_path(col_item, "instance_type") + field "number_of_instances", jmes_path(col_item, "number_of_instances") + field "scope", jmes_path(col_item, "scope") + field "tenancy", jmes_path(col_item, "tenancy") + field "offering_type", jmes_path(col_item, "offering_type") + end + end +end + +datasource "ds_filtered_aws_reservations" do + run_script $js_filtered_aws_reservations, $ds_accounts_by_billing_center, $ds_aws_reservations +end + +script "js_filtered_aws_reservations", type: "javascript" do + parameters "ds_accounts_by_billing_center", "ds_aws_reservations" + result "result" + code <<-EOS + bc_account_ids = _.uniq(_.pluck(ds_accounts_by_billing_center, "vendor_account_id")) + + result = _.filter(ds_aws_reservations, function(ri) { + return _.contains(bc_account_ids, ri['account_id']) + }) +EOS +end + +datasource "ds_account_list" do + run_script $js_account_list, $ds_accounts_by_billing_center, $ds_billing_centers +end + +script "js_account_list", type: "javascript" do + parameters "ds_accounts_by_billing_center", "ds_billing_centers" + result "result" + code <<-EOS + bc_object = {} + + _.each(ds_billing_centers, function(bc) { bc_object[bc['id']] = bc['name'] }) + + result = {} + + _.each(ds_accounts_by_billing_center, function(account) { + if (result[account['vendor_account_id']] == undefined) { + result[account['vendor_account_id']] = [] + } + + result[account['vendor_account_id']].push(bc_object[account['billing_center_id']]) + }) +EOS +end + +datasource "ds_expiring_aws_reservations" do + run_script $js_expiring_aws_reservations, $ds_filtered_aws_reservations, $ds_account_list, $ds_applied_policy, $param_days_expiration +end + +script "js_expiring_aws_reservations", type: "javascript" do + parameters "ds_filtered_aws_reservations", "ds_account_list", "ds_applied_policy", "param_days_expiration" + result "result" + code <<-'EOS' + result = [] + today = new Date() + + _.each(ds_filtered_aws_reservations, function(ri) { + expiration_date = new Date(ri['end_date']) + days_until_expiry = (expiration_date - today) / 1000 / 60 / 60 / 24 + if (days_until_expiry < 0) { days_until_expiry = 0 } + + if (days_until_expiry < param_days_expiration || days_until_expiry == 0) { + billing_centers = "" + + if (ds_account_list[ri['account_id']] != undefined) { + billing_centers = ds_account_list[ri['account_id']].join(', ') + } + + result.push({ + accountID: ri["account_id"], + accountName: ri["account_name"], + instanceType: ri["instance_type"], + instanceCount: ri["number_of_instances"], + resourceID: ri["lease_id"], + region: ri["region"], + expiration_date: expiration_date.toISOString().substring(0, 10), + days_until_expiry: Math.round(days_until_expiry), + billing_centers: billing_centers, + policy_name: ds_applied_policy["name"] + }) + } + }) +EOS +end + +############################################################################### +# Policy +############################################################################### + +policy "pol_ri_expiration" do + validate_each $ds_expiring_aws_reservations do + summary_template "{{ with index data 0 }}{{ .policy_name }}{{ end }}: AWS Expiring Reservations Found" + check eq(val(item, "resourceID"), "") + escalate $esc_email + export do + resource_level true + field "accountID" do + label "Account ID" + end + field "accountName" do + label "Account Name" + end + field "billing_centers" do + label "Associated Billing Center(s)" + end + field "region" do + label "Region" + end + field "resourceID" do + label "Lease ID" + end + field "instanceType" do + label "Instance Type" + end + field "instanceCount" do + label "Instance Count" + end + field "expiration_date" do + label "Expiration Date" + end + field "days_until_expiry" do + label "Days Until Expiration" + end + field "id" do + label "ID" + path "resourceID" + end + end + end +end + +############################################################################### +# Escalations +############################################################################### + +escalation "esc_email" do + automatic true + label "Send Email" + description "Send incident email" + email $param_email +end diff --git a/cost/aws/reserved_instances/expiration/expired_ris.pt b/cost/aws/reserved_instances/expiration/expired_ris.pt deleted file mode 100644 index 1bce6091eb..0000000000 --- a/cost/aws/reserved_instances/expiration/expired_ris.pt +++ /dev/null @@ -1,111 +0,0 @@ -name "AWS Expiring Reserved Instances" -rs_pt_ver 20180301 -type "policy" -short_description "**Deprecated: This policy is no longer being updated.** A policy that sends email notifications before AWS Reserved Instances expire. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/aws/reserved_instances/expiration) 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" -tenancy "single" -default_frequency "daily" -info( - version: "2.2.1", - provider: "AWS", - service: "Compute", - policy_set: "Reserved Instances", - deprecated: "true" -) - -############################################################################### -# Parameters -############################################################################### - -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_heads_up_days" do - type "number" - category "Policy Settings" - label "Days Prior To Expiration" - description "Number of days to prior to expiration date to trigger incident" - default 7 -end - -############################################################################### -# Authentication -############################################################################### - -credentials "auth_flexera" do - schemes "oauth2" - label "flexera" - description "Select Flexera One OAuth2 credentials" - tags "provider=flexera" -end - -############################################################################### -# Datasources & Scripts -############################################################################### - -datasource "ds_reservations" do - request do - auth $auth_flexera - host rs_optima_host - path join(["/reserved_instances/orgs/", rs_org_id, "/clouds/aws"]) - end - result do - encoding "json" - collect jmes_path(response, "[*]") do - field "end_datetime", jmes_path(col_item, "end_datetime") - field "account_name", jmes_path(col_item, "account_name") - field "account_id", jmes_path(col_item, "account_id") - field "region", jmes_path(col_item, "region") - field "instance_type", jmes_path(col_item, "instance_type") - field "instance_count", jmes_path(col_item, "number_of_instances") - end - end -end - -############################################################################### -# Policy -############################################################################### - -policy "pol_ri_expiration" do - validate_each $ds_reservations do - summary_template "{{ rs_project_name }} (Account ID: {{ rs_project_id }}): Reserved instances are nearing expiration." - export do - field "region" do - label "Region" - end - field "account_name" do - label "Account" - end - field "instance_type" do - label "Instance Type" - end - field "instance_count" do - label "Instance Count" - end - field "end_datetime" do - label "End Time" - end - end - - escalate $esc_email - check gt(dec(to_d(val(item, "end_datetime")), now), prod($param_heads_up_days, 24*3600)) - end -end - -############################################################################### -# Escalations -############################################################################### - -escalation "esc_email" do - automatic true - label "Send Email" - description "Send incident email" - email $param_email -end diff --git a/cost/azure/reserved_instances/expiration/CHANGELOG.md b/cost/azure/reserved_instances/expiration/CHANGELOG.md index 674ac53f88..29a55d7ac1 100644 --- a/cost/azure/reserved_instances/expiration/CHANGELOG.md +++ b/cost/azure/reserved_instances/expiration/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v3.0.0 + +- Policy is no longer deprecated +- Added ability to filter results by Billing Center +- Additional fields added to incident to provide more context +- Streamlined code for better readability and faster execution + ## v2.2.1 - Added `deprecated` field to policy metadata. Functionality is unchanged. diff --git a/cost/azure/reserved_instances/expiration/README.md b/cost/azure/reserved_instances/expiration/README.md index 72d2a140cf..2524991d53 100644 --- a/cost/azure/reserved_instances/expiration/README.md +++ b/cost/azure/reserved_instances/expiration/README.md @@ -1,39 +1,33 @@ # Azure Expiring Reserved Instances -## Deprecated +## What It Does -This policy is no longer being updated. +This policy template produces a report of all Azure reservations that have expired or are going to expire within a user-specified number of days. Optionally, this report can be emailed. -## What it does +## Input Parameters + +- *Email Addresses* - Email addresses of the recipients you wish to notify when new incidents are created. +- *Days Until Expiration* - The number of days until expiration to include a Reservation in the report. Set to `0` to only report expired Reservations. +- *Allow/Deny Billing Centers* - Allow or Deny entered Billing Centers. +- *Allow/Deny Billing Center List* - A list of allowed or denied Billing Center names/IDs. Leave blank to report on Reservations in all Billing Centers. -This Policy Template leverages the Optima Bill Data Azure Reserved Instances. It will notify only if expiration is within the time frame specified in `Number of days to prior to expiration date to trigger incident` field. It will email the user specified in Email addresses of the recipients you wish to notify. +## Policy Actions + +- Send an email report ## Prerequisites 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). - [**Flexera Credential**](https://docs.flexera.com/flexera/EN/Automation/ProviderCredentials.htm) (*provider=flexera*) which has the following roles: - - `ca_user` + - `billing_center_viewer` 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. -## Input Parameters - -This policy has the following input parameters required when launching the policy. - -- *Email addresses of the recipients you wish to notify* - A list of email addresses to notify -- *Number of days to prior to expiration date to trigger incident* - Number of days before a RI expires to alert on - -## Policy Actions - -The following policy actions are taken on any resources found to be out of compliance. - -- Send an email report - ## Supported Clouds - Azure ## Cost -This Policy Template does not launch any instances, and so does not incur any cloud costs. +This Policy Template does not incur any cloud costs. diff --git a/cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt b/cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt index 1c30599473..95c67bad3a 100644 --- a/cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt +++ b/cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt @@ -1,42 +1,54 @@ name "Azure Expiring Reserved Instances" rs_pt_ver 20180301 type "policy" -short_description "**Deprecated: This policy is no longer being updated.** A policy that sends email notifications when an Azure Reserved Instance are about to expire. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/azure/reserved_instances/expiration) and [docs.flexera.com/flexera/EN/Automation](https://docs.flexera.com/flexera/EN/Automation/AutomationGS.htm) to learn more." +short_description "Reports on Azure Reserved Instances that have or will soon expire. See the [README](https://github.com/flexera-public/policy_templates/tree/master/cost/azure/reserved_instances/expiration) and [docs.flexera.com/flexera/EN/Automation](https://docs.flexera.com/flexera/EN/Automation/AutomationGS.htm) to learn more." long_description "" -severity "low" category "Cost" -tenancy "single" +severity "medium" default_frequency "daily" info( - version: "2.2.1", - provider: "Flexera Optima", - service: "", - policy_set: "", - deprecated: "true" + version: "3.0.0", + provider: "Azure", + service: "Compute", + policy_set: "Reserved Instances" ) ############################################################################### -# Permissions +# Parameters ############################################################################### -permission "optima" do - label "Access Optima Resources" - resources "rs_optima.aws_reserved_instances" - actions "rs_optima.index" +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 -############################################################################### -# Parameters -############################################################################### - parameter "param_days_expiration" do type "number" - label "Number of days to prior to expiration date to trigger incident" + category "Policy Settings" + label "Days Until Expiration" + description "The number of days until expiration to include a Reservation in the report. Set to '0' to only report expired Reservations." + min_value 0 + default 15 end -parameter "param_email" do +parameter "param_bc_allow_or_deny" do + type "string" + category "Filters" + label "Allow/Deny Billing Centers" + description "Allow or Deny entered Billing Centers." + allowed_values "Allow", "Deny" + default "Allow" +end + +parameter "param_billing_centers" do type "list" - label "Email addresses of the recipients you wish to notify" + category "Filters" + label "Allow/Deny Billing Center List" + description "A list of allowed or denied Billing Center names/IDs. Leave blank to report on Reservations in all Billing Centers." + default [] end ############################################################################### @@ -45,86 +57,248 @@ end credentials "auth_flexera" do schemes "oauth2" - label "flexera" + label "Flexera" description "Select Flexera One OAuth2 credentials" tags "provider=flexera" end ############################################################################### -# Pagination +# Datasources & Scripts ############################################################################### -############################################################################### -# Datasources -############################################################################### +# Get applied policy metadata for use later +datasource "ds_applied_policy" do + request do + 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 +end -datasource "ds_reservations" do +datasource "ds_billing_centers" do request do auth $auth_flexera host rs_optima_host - path join(["/reserved_instances/orgs/",rs_org_id,"/clouds/azure/summary"]) + path join(["/analytics/orgs/", rs_org_id, "/billing_centers"]) + header "Api-Version", "1.0" + header "User-Agent", "RS Policies" end result do encoding "json" - collect jmes_path(response,"[*]") do - field "account_name", jmes_path(col_item,"account_name") - field "arm_sku_name", jmes_path(col_item,"arm_sku_name") - field "expiration_date", jmes_path(col_item,"expiration_date") - field "purchasing_subscription_name", jmes_path(col_item,"purchasing_subscription_name") - field "purchasing_subscription_guid", jmes_path(col_item,"purchasing_subscription_guid") - field "quantity", jmes_path(col_item,"quantity") - field "region", jmes_path(col_item,"region") - field "reservation_id", jmes_path(col_item,"reservation_id") - field "reservation_order_id", jmes_path(col_item,"reservation_order_id") - field "reservation_order_name", jmes_path(col_item,"reservation_order_name") - field "term", jmes_path(col_item,"term") + 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 end end -datasource "ds_filtered_reservations" do - run_script $js_filtered_reservations, $ds_reservations, $param_days_expiration +datasource "ds_billing_centers_filtered" do + run_script $js_billing_centers_filtered, $ds_billing_centers, $param_bc_allow_or_deny, $param_billing_centers end -############################################################################### -# Scripts -############################################################################### +script "js_billing_centers_filtered", type: "javascript" do + parameters "ds_billing_centers", "param_bc_allow_or_deny", "param_billing_centers" + result "result" + code <<-EOS + allow_deny_test = { "Allow": true, "Deny": false } + + if (param_billing_centers.length > 0) { + billing_centers = _.filter(ds_billing_centers, function(item) { + id_found = _.contains(param_billing_centers, item['id']) == allow_deny_test[param_bc_allow_or_deny] + name_found = _.contains(param_billing_centers, item['name']) == allow_deny_test[param_bc_allow_or_deny] + return id_found || name_found + }) + + // Check for conflicting parents/children and remove children if present + bc_ids = _.compact(_.pluck(billing_centers, 'id')) + bad_children = _.filter(ds_billing_centers, function(bc) { return _.contains(bc_ids, bc['parent_id']) }) + bad_children_ids = _.pluck(bad_children, 'id') + + // Create final result with the bad children removed + final_list = _.reject(billing_centers, function(bc) { return _.contains(bad_children_ids, bc['id']) }) + } else { + // If we're not filtering at all, just grab all of the top level billing centers + final_list = _.filter(ds_billing_centers, function(bc) { + return bc['parent_id'] == null || bc['parent_id'] == undefined + }) + } + + result = _.compact(_.pluck(final_list, 'id')) +EOS +end + +datasource "ds_accounts_by_billing_center" do + request do + run_script $js_accounts_by_billing_center, $ds_billing_centers_filtered, rs_org_id, rs_optima_host + end + result do + encoding "json" + collect jmes_path(response, "rows[*]") do + field "cost", jmes_path(col_item, "metrics.cost_amortized_unblended_adj") + field "billing_center_id", jmes_path(col_item, "dimensions.billing_center_id") + field "vendor_account_id", jmes_path(col_item, "dimensions.vendor_account") + field "vendor_account_name", jmes_path(col_item, "dimensions.vendor_account_name") + field "timestamp", jmes_path(col_item, "timestamp") + end + end +end + +script "js_accounts_by_billing_center", type: "javascript" do + parameters "ds_billing_centers_filtered", "rs_org_id", "rs_optima_host" + result "request" + code <<-EOS + start_date = new Date().toISOString() + start_date = start_date.split('-')[0] + '-' + start_date.split('-')[1] + + end_year = Number(start_date.split('-')[0]) + end_month = Number(start_date.split('-')[1]) + 1 + if (end_month == 13) { end_month = 1; end_year++ } + if (end_month < 10) { end_month = '0' + end_month.toString() } + end_date = end_year.toString() + '-' + end_month.toString() + + var request = { + auth: "auth_flexera", + verb: "POST", + host: rs_optima_host, + path: "/bill-analysis/orgs/" + rs_org_id + "/costs/aggregated", + body_fields: { + "dimensions": [ "billing_center_id", "vendor_account", "vendor_account_name" ], + "granularity": "month", + "metrics": [ "cost_amortized_unblended_adj" ], + "billing_center_ids": ds_billing_centers_filtered, + "start_at": start_date, + "end_at": end_date + } + } +EOS +end -script "js_filtered_reservations", type: "javascript" do - parameters "reservations","param_days_expiration" - result "content" +datasource "ds_azure_reservations" do + request do + auth $auth_flexera + host rs_optima_host + path join(["/reserved_instances/orgs/", rs_org_id, "/clouds/azure"]) + end + result do + encoding "json" + collect jmes_path(response, "[*]") do + field "id", jmes_path(col_item, "id") + field "account_name", jmes_path(col_item, "account_name") + field "amount", jmes_path(col_item, "amount") + field "arm_sku_name", jmes_path(col_item, "arm_sku_name") + field "billing_frequency", jmes_path(col_item, "billing_frequency") + field "cost_center", jmes_path(col_item, "cost_center") + field "currency", jmes_path(col_item, "currency") + field "current_enrollment", jmes_path(col_item, "current_enrollment") + field "department_name", jmes_path(col_item, "department_name") + field "description", jmes_path(col_item, "description") + field "event_date", jmes_path(col_item, "event_date") + field "event_type", jmes_path(col_item, "event_type") + field "expiration_date", jmes_path(col_item, "expiration_date") + field "monthly_cost", jmes_path(col_item, "monthly_cost") + field "purchased_quantity", jmes_path(col_item, "purchased_quantity") + field "purchasing_enrollment", jmes_path(col_item, "purchasing_enrollment") + field "purchasing_subscription_guid", jmes_path(col_item, "purchasing_subscription_guid") + field "purchasing_subscription_name", jmes_path(col_item, "purchasing_subscription_name") + field "quantity", jmes_path(col_item, "quantity") + field "region", jmes_path(col_item, "region") + field "remaining_quantity", jmes_path(col_item, "remaining_quantity") + field "reservation_order_id", jmes_path(col_item, "reservation_order_id") + field "reservation_order_name", jmes_path(col_item, "reservation_order_name") + field "term", jmes_path(col_item, "term") + field "util_avg_percentage", jmes_path(col_item, "utilization.avg_utilization_percentage") + field "util_max_percentage", jmes_path(col_item, "utilization.max_utilization_percentage") + field "util_min_percentage", jmes_path(col_item, "utilization.min_utilization_percentage") + field "util_reservation_id", jmes_path(col_item, "utilization.reservation_id") + field "util_reserved_hours", jmes_path(col_item, "utilization.reserved_hours") + field "util_sku_name", jmes_path(col_item, "utilization.sku_name") + field "util_usage_date", jmes_path(col_item, "utilization.usage_date") + field "util_used_hours", jmes_path(col_item, "utilization.used_hours") + end + end +end + +datasource "ds_filtered_azure_reservations" do + run_script $js_filtered_azure_reservations, $ds_accounts_by_billing_center, $ds_azure_reservations +end + +script "js_filtered_azure_reservations", type: "javascript" do + parameters "ds_accounts_by_billing_center", "ds_azure_reservations" + result "result" + code <<-EOS + bc_account_ids = _.uniq(_.pluck(ds_accounts_by_billing_center, "vendor_account_id")) + + result = _.filter(ds_azure_reservations, function(ri) { + return _.contains(bc_account_ids, ri['purchasing_subscription_guid']) + }) +EOS +end + +datasource "ds_account_list" do + run_script $js_account_list, $ds_accounts_by_billing_center, $ds_billing_centers +end + +script "js_account_list", type: "javascript" do + parameters "ds_accounts_by_billing_center", "ds_billing_centers" + result "result" code <<-EOS - var content=[]; - var now = new Date(); - var now_ms = now.getTime(); - var one_day=1000*60*60*24; - var date1_ms = now.getTime(); - - for (var i = 0; i < reservations.length ; i++) { - reservation = reservations[i] - var date2_ms = (new Date(reservation['expiration_date'])).getTime(); - var difference_ms = date2_ms - date1_ms; - var daysLeft = Math.round(difference_ms/one_day); - - if (daysLeft <= param_days_expiration && now_ms <= date2_ms) { - content.push({ - account_name: reservation['account_name'], - arm_sku_name: reservation['arm_sku_name'], - expiration_date: reservation['expiration_date'], - purchasing_subscription_name: reservation['purchasing_subscription_name'], - purchasing_subscription_guid: reservation['purchasing_subscription_guid'], - quantity: reservation['quantity'], - region: reservation['region'], - reservation_id: reservation['reservation_id'], - reservation_order_id: reservation['reservation_order_id'], - reservation_order_name: reservation['reservation_order_name'], - term: reservation['term'], - daysExpiration: daysLeft - }) + bc_object = {} + + _.each(ds_billing_centers, function(bc) { bc_object[bc['id']] = bc['name'] }) + + result = {} + + _.each(ds_accounts_by_billing_center, function(account) { + if (result[account['vendor_account_id']] == undefined) { + result[account['vendor_account_id']] = [] + } + + result[account['vendor_account_id']].push(bc_object[account['billing_center_id']]) + }) +EOS +end + +datasource "ds_expiring_azure_reservations" do + run_script $js_expiring_azure_reservations, $ds_filtered_azure_reservations, $ds_account_list, $ds_applied_policy, $param_days_expiration +end + +script "js_expiring_azure_reservations", type: "javascript" do + parameters "ds_filtered_azure_reservations", "ds_account_list", "ds_applied_policy", "param_days_expiration" + result "result" + code <<-'EOS' + result = [] + today = new Date() + + _.each(ds_filtered_azure_reservations, function(ri) { + expiration_date = new Date(ri['expiration_date']) + days_until_expiry = (expiration_date - today) / 1000 / 60 / 60 / 24 + if (days_until_expiry < 0) { days_until_expiry = 0 } + + if (days_until_expiry < param_days_expiration || days_until_expiry == 0) { + billing_centers = "" + + if (ds_account_list[ri['purchasing_subscription_guid']] != undefined) { + billing_centers = ds_account_list[ri['purchasing_subscription_guid']].join(', ') } + + result.push({ + accountID: ri["purchasing_subscription_guid"], + accountName: ri["purchasing_subscription_name"], + instanceType: ri["util_sku_name"], + instanceCount: Math.round(ri["amount"]), + resourceID: ri["util_reservation_id"], + region: ri["region"], + term: ri["term"], + expiration_date: expiration_date.toISOString().substring(0, 10), + days_until_expiry: Math.round(days_until_expiry), + billing_centers: billing_centers, + policy_name: ds_applied_policy["name"] + }) } - content = _.sortBy(content, 'reservation_id'); - content = _.sortBy(content, 'reservation_order_id'); + }) EOS end @@ -132,44 +306,47 @@ end # Policy ############################################################################### -policy "ri_expiration" do - validate $ds_filtered_reservations do - summary_template "{{ rs_org_name }} (Org ID: {{ rs_org_id }}): {{ len data }} Reserved Instance(s) in Microsoft Azure Expiring" - escalate $report_expiring_reserved_instances - check eq(size(data),0) +policy "pol_ri_expiration" do + validate_each $ds_expiring_azure_reservations do + summary_template "{{ with index data 0 }}{{ .policy_name }}{{ end }}: Azure Expiring Reservations Found" + check eq(val(item, "resourceID"), "") + escalate $esc_email export do resource_level true - field "id" do - label "Reservation Id" - path "reservation_id" - end - field "reservation_order_id" do - label "Reservation Order Id" + field "accountID" do + label "Subscription ID" end - field "reservation_order_name" do - label "Reservation Order Name" - end - field "purchasing_subscription_name" do + field "accountName" do label "Subscription Name" end - field "purchasing_subscription_guid" do - label "Subscription GUID" + field "billing_centers" do + label "Associated Billing Center(s)" end field "region" do - label "Location" + label "Region" + end + field "term" do + label "Term" + end + field "resourceID" do + label "Lease ID" end - field "arm_sku_name" do - label "ARM SKU" + field "instanceType" do + label "Instance Type" end - field "quantity" do - label "Quantity" + field "instanceCount" do + label "Instance Count" end field "expiration_date" do label "Expiration Date" end - field "daysExpiration" do + field "days_until_expiry" do label "Days Until Expiration" end + field "id" do + label "ID" + path "resourceID" + end end end end @@ -178,7 +355,7 @@ end # Escalations ############################################################################### -escalation "report_expiring_reserved_instances" do +escalation "esc_email" do automatic true label "Send Email" description "Send incident email" diff --git a/data/policy_permissions_list/master_policy_permissions_list.json b/data/policy_permissions_list/master_policy_permissions_list.json index 56eb3705d1..853a7019bf 100644 --- a/data/policy_permissions_list/master_policy_permissions_list.json +++ b/data/policy_permissions_list/master_policy_permissions_list.json @@ -1359,25 +1359,15 @@ ] }, { - "id": "./cost/aws/reserved_instances/expiration/expired_ris.pt", + "id": "./cost/aws/reserved_instances/expiration/aws_reserved_instance_expiration.pt", "name": "AWS Expiring Reserved Instances", - "version": "2.2.1", + "version": "3.0.0", "providers": [ { "name": "flexera", "permissions": [ { - "name": "actor", - "read_only": true, - "required": true - }, - { - "name": "observer", - "read_only": true, - "required": true - }, - { - "name": "credential_viewer", + "name": "billing_center_viewer", "read_only": true, "required": true } @@ -2456,6 +2446,23 @@ } ] }, + { + "id": "./cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt", + "name": "Azure Expiring Reserved Instances", + "version": "3.0.0", + "providers": [ + { + "name": "flexera", + "permissions": [ + { + "name": "billing_center_viewer", + "read_only": true, + "required": true + } + ] + } + ] + }, { "id": "./cost/azure/reserved_instances/recommendations/azure_reserved_instance_recommendations.pt", "name": "Azure Reserved Instances Recommendations", diff --git a/data/policy_permissions_list/master_policy_permissions_list.yaml b/data/policy_permissions_list/master_policy_permissions_list.yaml index 5d69b4c8f7..e777659100 100644 --- a/data/policy_permissions_list/master_policy_permissions_list.yaml +++ b/data/policy_permissions_list/master_policy_permissions_list.yaml @@ -803,19 +803,13 @@ - name: billing_center_viewer read_only: true required: true -- id: "./cost/aws/reserved_instances/expiration/expired_ris.pt" +- id: "./cost/aws/reserved_instances/expiration/aws_reserved_instance_expiration.pt" name: AWS Expiring Reserved Instances - version: 2.2.1 + version: 3.0.0 :providers: - :name: flexera :permissions: - - name: actor - read_only: true - required: true - - name: observer - read_only: true - required: true - - name: credential_viewer + - name: billing_center_viewer read_only: true required: true - id: "./cost/aws/reserved_instances/recommendations/aws_reserved_instance_recommendations.pt" @@ -1448,6 +1442,15 @@ - name: billing_center_viewer read_only: true required: true +- id: "./cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt" + name: Azure Expiring Reserved Instances + version: 3.0.0 + :providers: + - :name: flexera + :permissions: + - name: billing_center_viewer + read_only: true + required: true - id: "./cost/azure/reserved_instances/recommendations/azure_reserved_instance_recommendations.pt" name: Azure Reserved Instances Recommendations version: '3.4' diff --git a/tools/policy_master_permission_generation/validated_policy_templates.yaml b/tools/policy_master_permission_generation/validated_policy_templates.yaml index 83ba76b116..a260e47cdb 100644 --- a/tools/policy_master_permission_generation/validated_policy_templates.yaml +++ b/tools/policy_master_permission_generation/validated_policy_templates.yaml @@ -24,7 +24,7 @@ validated_policy_templates: - "./cost/aws/rightsize_ec2_instances/aws_rightsize_ec2_instances.pt" - "./cost/aws/rightsize_rds_instances/aws_rightsize_rds_instances.pt" - "./cost/aws/reserved_instances/coverage/reserved_instance_coverage.pt" -- "./cost/aws/reserved_instances/expiration/expired_ris.pt" +- "./cost/aws/reserved_instances/expiration/aws_reserved_instance_expiration.pt" - "./cost/aws/reserved_instances/recommendations/aws_reserved_instance_recommendations.pt" - "./cost/aws/reserved_instances/utilization/utilization_ris.pt" - "./cost/aws/s3_bucket_size/aws_bucket_size.pt" @@ -70,6 +70,7 @@ validated_policy_templates: - "./cost/azure/hybrid_use_benefit_sql/ahub_sql.pt" - "./cost/azure/idle_compute_instances/azure_idle_compute_instances.pt" - "./cost/azure/old_snapshots/azure_delete_old_snapshots.pt" +- "./cost/azure/reserved_instances/expiration/azure_reserved_instance_expiration.pt" - "./cost/azure/reserved_instances/recommendations/azure_reserved_instance_recommendations.pt" - "./cost/azure/reserved_instances/utilization/azure_reserved_instance_utilization.pt" - "./cost/azure/rightsize_compute_instances/azure_compute_rightsizing.pt"