From cec390f93efb43a097461c98d76a8a8597d6a51a Mon Sep 17 00:00:00 2001 From: lchrdn <89968908+lchrdn@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:26:46 +0200 Subject: [PATCH] (plugin) cloud::azure::management::cost - adding mode to query subscription costs (#4024) * initial commit for azure query costs * add condition for global sub costs counter * adding costs per resource group * peaufinage and adding help for query costs * modify name of output functions * remove extra line in api.pm * enh explanations * enh condition to fix display bug * Cosmetic things * remove useless condition on tag value * remove old mode path * minor fixes * minor fixes again on counters condition * modify help according to new mode name * rename perfdata for compliance Co-authored-by: Simon Bomm --- centreon-plugins/cloud/azure/custom/api.pm | 27 ++ .../management/costs/mode/costsexplorer.pm | 295 ++++++++++++++++++ .../cloud/azure/management/costs/plugin.pm | 5 +- 3 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 centreon-plugins/cloud/azure/management/costs/mode/costsexplorer.pm diff --git a/centreon-plugins/cloud/azure/custom/api.pm b/centreon-plugins/cloud/azure/custom/api.pm index 5294a8131..76bc80f95 100644 --- a/centreon-plugins/cloud/azure/custom/api.pm +++ b/centreon-plugins/cloud/azure/custom/api.pm @@ -306,6 +306,33 @@ sub json_decode { return $decoded; } + +sub azure_get_subscription_cost_management_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription} . "/providers/Microsoft.CostManagement/query?api-version=" . $self->{api_version} ; + + return $url; +} + +sub azure_get_subscription_cost_management { + my ($self, %options) = @_; + + my $results = {}; + my $encoded_form_post; + + my $full_url = $self->azure_get_subscription_cost_management_set_url(); + + eval { + $encoded_form_post = JSON::XS->new->utf8->encode($options{body_post}); + }; + + $self->{http}->add_header(key => 'Content-Type', value => 'application/json'); + my $response = $self->request_api(method => 'POST', full_url => $full_url, query_form_post => $encoded_form_post, hostname => ''); + + return $response->{properties}->{rows}; +} + sub azure_get_metrics_set_url { my ($self, %options) = @_; diff --git a/centreon-plugins/cloud/azure/management/costs/mode/costsexplorer.pm b/centreon-plugins/cloud/azure/management/costs/mode/costsexplorer.pm new file mode 100644 index 000000000..4ca72e354 --- /dev/null +++ b/centreon-plugins/cloud/azure/management/costs/mode/costsexplorer.pm @@ -0,0 +1,295 @@ +# +# Copyright 2022 Centreon (http://www.centreon.com/) +# +# Centreon is a full-fledged industry-strength solution that meets +# the needs in IT infrastructure and application monitoring for +# service performance. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +package cloud::azure::management::costs::mode::costsexplorer; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use DateTime; + +sub resource_group_prefix_output { + my ($self, %options) = @_; + + return sprintf( "Resource group '%s' ", $options{instance}); +} + +sub custom_subscription_output { + my ($self, %options) = @_; + + return sprintf( "Subscription costs for specified period: %.2f %s", $self->{result_values}->{subscription_cost}, $self->{result_values}->{currency}); +} + +sub custom_resource_group_output { + my ($self, %options) = @_; + + return sprintf( "costs for specified period: %.2f %s", $self->{result_values}->{resource_group_cost}, $self->{result_values}->{currency}); +} + + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'global', type => 0, skipped_code => { -10 => 1 } }, + { name => 'resource_group', type => 1, cb_prefix_output => 'resource_group_prefix_output', message_multiple => 'All resource group costs are OK', skipped_code => { -10 => 1 } }, + ]; + + $self->{maps_counters}->{global} = [ + { label => 'subscription-costs', nlabel => 'azure.subscription.global.costs', set => { + key_values => [ { name => 'subscription_cost' }, { name => 'currency' } ], + closure_custom_output => $self->can('custom_subscription_output'), + perfdatas => [ + { template => '%.2f', min => 0 } + ] + } + } + ]; + $self->{maps_counters}->{resource_group} = [ + { label => 'resource-group-costs', nlabel => 'azure.resourcegroup.costs', set => { + key_values => [ { name => 'resource_group_cost' }, { name => 'currency'} ], + closure_custom_output => $self->can('custom_resource_group_output'), + perfdatas => [ + { template => '%.2f', min => 0, label_extra_instance => 1 }, + ] + } + } + ] + +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perfdata => 1); + bless $self, $class; + + $options{options}->add_options(arguments => { + "lookup-days:s" => { name => 'lookup_days', default => 30 }, + "resource-group:s@" => { name => 'resource_group' }, + "tags:s@" => { name => 'tags' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + if (defined($self->{option_results}->{tags}) && $self->{option_results}->{tags} ne '') { + foreach my $tag_pair (@{$self->{option_results}->{tags}}) { + my ($key, $value) = split / => /, $tag_pair; + next if !defined($key); + $value = "" if !defined($value); + centreon::plugins::misc::trim($value); + push @{$self->{tags}->{ centreon::plugins::misc::trim($key) }}, $value; + } + } +} + +sub create_body_filter { + my ($self, %options) = @_; + + my $filter; + my $resource_group_filter; + + # group by resource group if we want costs for one or multiple resource groups + if (defined($self->{option_results}->{resource_group}) && $self->{option_results}->{resource_group} ne ""){ + $resource_group_filter->{dimensions}->{name} = "ResourceGroup"; + $resource_group_filter->{dimensions}->{operator} = "In"; + $resource_group_filter->{dimensions}->{values} = $self->{option_results}->{resource_group}; + } + + # if we have more than two elements to filter on (either several tags, or a tag and some resource groups) we need to have a 'and' array containing the elements + if ((defined($self->{tags}) && keys %{$self->{tags}} > 0) && (defined($self->{option_results}->{resource_group}) && $self->{option_results}->{resource_group} ne "") + || (defined($self->{tags}) && keys %{$self->{tags}} > 1)){ + foreach my $tag (keys %{$self->{tags}}){ + push @{$filter->{and}}, my $tags_filter = { + tags => { + name => $tag, + operator => "In", + values => $self->{tags}->{$tag} + } + }; + } + push @{$filter->{and}}, $resource_group_filter if keys %{$resource_group_filter} > 0; # we add a filter for resource group if at least one resource group was specified + return $filter; + } + + # if we have at least one tag and no resource group we need to set the filter on this tag, without the 'and' + if (defined($self->{tags}) && keys %{$self->{tags}} >= 0 && (!defined($self->{option_results}->{resource_group}) || $self->{option_results}->{resource_group} eq "")){ + foreach my $tag (keys %{$self->{tags}}){ + $filter = { + tags => { + name => $tag, + operator => "In", + values => $self->{tags}->{$tag} + } + }; + } + return $filter; + } + + # if we only have resource group(s) specified to filter on, then we return the filter for resource group(s) + $filter = $resource_group_filter; + return $filter; +} + +sub create_body_payload { + my ($self, %options) = @_; + + my $start_date = DateTime->now->subtract( days => $self->{option_results}->{lookup_days} ); + my $end_date = DateTime->now; + + my $form_post = { + "type" => "Usage", + "timeframe" => "Custom", + "dataset" => { + "aggregation" => { + "totalCost" => { + "name" => "PreTaxCost", + "function" => "Sum" + } + }, + "granularity" => "daily", + } + }; + + $form_post->{timeperiod}->{from} = $start_date->ymd; + $form_post->{timeperiod}->{to} = $end_date->ymd; + + if ((defined($self->{option_results}->{resource_group}) && $self->{option_results}->{resource_group} ne "") || + ((defined($self->{tags}) && keys %{$self->{tags}} > 0))){ + my $filter; + $filter = $self->create_body_filter(); + $form_post->{dataset}->{filter} = $filter if defined($filter); + push @{$form_post->{dataset}->{grouping}}, { "type" => "Dimension", "name" => "ResourceGroup"}; + } + return $form_post; +} + + +sub manage_selection { + my ($self, %options) = @_; + + my $raw_form_post = $self->create_body_payload(); + my $costs = $options{custom}->azure_get_subscription_cost_management(body_post => $raw_form_post); + + my $sum_costs; + my $currency; + + if ((!defined($self->{option_results}->{resource_group}) || $self->{option_results}->{resource_group} eq "") + && keys %{$self->{tags}} == 0){ + foreach my $daily_subscription_cost (@{$costs}){ + $sum_costs += ${$daily_subscription_cost}[0]; + $currency = ${$daily_subscription_cost}[2]; + } + $self->{global} = { + subscription_cost => $sum_costs, + currency => $currency + }; + } + if (defined($self->{option_results}->{resource_group}) || defined($self->{option_results}->{tags}) && keys %{$self->{tags}} > 0){ + my $resource_group_total_costs; + + foreach my $daily_resource_group_cost (@{$costs}){ + my $resource_group = ${$daily_resource_group_cost}[2]; + $sum_costs += ${$daily_resource_group_cost}[0]; + $currency = ${$daily_resource_group_cost}[3]; + + $resource_group_total_costs->{$resource_group}->{sum} = $sum_costs; + $resource_group_total_costs->{$resource_group}->{currency} = $currency; + } + + foreach my $resource_group (keys %$resource_group_total_costs){ + $self->{resource_group}->{$resource_group} = { + resource_group_cost => $resource_group_total_costs->{$resource_group}->{sum}, + currency => $resource_group_total_costs->{$resource_group}->{currency} + }; + } + } + if (scalar(keys %{$self->{global}}) <= 0 && scalar(keys %{$self->{resource_group}}) <= 0) { + $self->{output}->add_option_msg(short_msg => "No entry found."); + $self->{output}->option_exit(); + } +} + +1; + +__END__ + +=head1 MODE + +Check costs for a subscription or per resource group. + +If you don't specify a resource group or any tags then you will have costs for the whole subscription. + +You can specify resource groups and tags to filter on. + +Example to get costs per subscription for the last 30 days: +perl centreon_plugins.pl --plugin=cloud::azure::management::costs::plugin --mode=costs-explorer --custommode=api --client-id='xxx' +--client-secret='xxx' --tenant='xxx' --subscription='xxx' --lookup-days=30 + +Example to get costs for a resource group: +perl centreon_plugins.pl --plugin=cloud::azure::management::costs::plugin --mode=costs-explorer --custommode=api --client-id='xxx' +--client-secret='xxx' --tenant='xxx' --subscription='xxx' --lookup-days=30 --resource-group=MYRESOURCEGROUP --tags='Environment => integration' + +=over 8 + +=item B<--resource-group> + +Set resource group (Optional). + +If you don't, you get costs for the whole subscription. + +You can specify multiple resource groups. You will get results for each one of the resource groups specified. +Example: --resource-group=MYRESOURCEGROUP1 --resource-group=MYRESOURCEGROUP2 + +=item B<--tags> + +Set tags to filter on (Optional). + +You can specify multiple tags. You will get results for the resource groups matching all the tags specified. +Example: --tags='Environment => DEV' --tags='managed_by => automation' + +=item B<--lookup-days> + +Days backward to look up (Default: '30'). + +=item B<--warning-subscription-costs> + +Set warning threshold for subscription costs. + +=item B<--critical-subscription-costs> + +Set critical threshold for subscription costs. + +=item B<--warning-resource-group-costs> + +Set warning threshold for resource groups costs. + +=item B<--critical-resource-group-costs> + +Set critical threshold for resource groups costs. + +=back + +=cut diff --git a/centreon-plugins/cloud/azure/management/costs/plugin.pm b/centreon-plugins/cloud/azure/management/costs/plugin.pm index f51a01728..a2af428dc 100644 --- a/centreon-plugins/cloud/azure/management/costs/plugin.pm +++ b/centreon-plugins/cloud/azure/management/costs/plugin.pm @@ -31,8 +31,9 @@ sub new { $self->{version} = '0.1'; %{ $self->{modes} } = ( - 'budgets' => 'cloud::azure::management::costs::mode::budgets', - 'list-budgets' => 'cloud::azure::management::costs::mode::listbudgets' + 'budgets' => 'cloud::azure::management::costs::mode::budgets', + 'costs-explorer' => 'cloud::azure::management::costs::mode::costsexplorer', + 'list-budgets' => 'cloud::azure::management::costs::mode::listbudgets' ); $self->{custom_modes}{api} = 'cloud::azure::custom::api';