From 558e2404dfc0a0ea34ba218ba3b557822c16c258 Mon Sep 17 00:00:00 2001 From: Simon Bomm Date: Thu, 27 Oct 2022 14:47:47 +0200 Subject: [PATCH] (plugin) cloud::azure::management::costs - new modes to check azure config compliance (#4019) * Add files via upload * Add files via upload * Update tagonresources.pm * (wip)enhance-matoy-contribution * (wip) * Final versions and bugfixes * Remove unused option * Fix after @matoy review * Rework tag compliance check * + Add latest @matoy feedback * + garbage Co-authored-by: matoy --- centreon-plugins/cloud/azure/custom/api.pm | 279 ++++++++++++- .../management/costs/mode/hybridbenefits.pm | 319 +++++++++++++++ .../management/costs/mode/orphanresources.pm | 385 ++++++++++++++++++ .../management/costs/mode/tagscompliance.pm | 214 ++++++++++ .../cloud/azure/management/costs/plugin.pm | 9 +- 5 files changed, 1188 insertions(+), 18 deletions(-) create mode 100644 centreon-plugins/cloud/azure/management/costs/mode/hybridbenefits.pm create mode 100644 centreon-plugins/cloud/azure/management/costs/mode/orphanresources.pm create mode 100644 centreon-plugins/cloud/azure/management/costs/mode/tagscompliance.pm diff --git a/centreon-plugins/cloud/azure/custom/api.pm b/centreon-plugins/cloud/azure/custom/api.pm index 76bc80f95..a47dcd59d 100644 --- a/centreon-plugins/cloud/azure/custom/api.pm +++ b/centreon-plugins/cloud/azure/custom/api.pm @@ -530,18 +530,27 @@ sub azure_list_vms_set_url { my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); - $url .= "/providers/Microsoft.Compute/virtualMachines?api-version=" . $self->{api_version}; - + $url .= "/providers/Microsoft.Compute/virtualMachines"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; return $url; } sub azure_list_vms { my ($self, %options) = @_; - + + my $full_response = []; my $full_url = $self->azure_list_vms_set_url(%options); - my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); - - return $response->{value}; + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; } sub azure_list_groups_set_url { @@ -709,18 +718,27 @@ sub azure_list_sqlservers_set_url { my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); - $url .= "/providers/Microsoft.Sql/servers?api-version=" . $self->{api_version}; - + $url .= "/providers/Microsoft.Sql/servers"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; return $url; } sub azure_list_sqlservers { my ($self, %options) = @_; + my $full_response = []; my $full_url = $self->azure_list_sqlservers_set_url(%options); - my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } - return $response->{value}; + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; } sub azure_list_sqldatabases_set_url { @@ -729,18 +747,26 @@ sub azure_list_sqldatabases_set_url { my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); $url .= "/providers/Microsoft.Sql/servers/" . $options{server} if (defined($options{server}) && $options{server} ne ''); - $url .= "/databases?api-version=" . $self->{api_version}; - + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; return $url; } sub azure_list_sqldatabases { my ($self, %options) = @_; + my $full_response = []; my $full_url = $self->azure_list_sqldatabases_set_url(%options); - my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } - return $response->{value}; + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; } sub azure_get_log_analytics_set_url { @@ -864,7 +890,6 @@ sub azure_get_usagedetails_set_url { $url .= "/providers/Microsoft.Consumption/usageDetails?\$filter=properties%2FusageStart ge %27" . $options{usage_start} . "%27 and properties%2FusageEnd le %27" . $options{usage_end} . "%27"; $url .= "&metric=actualcost"; $url .= "&api-version=" . $self->{api_version}; - return $url; } @@ -886,6 +911,230 @@ sub azure_get_usagedetails { return $full_response; } +sub azure_list_compute_disks_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Compute/disks"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_compute_disks { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_compute_disks_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_nics_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Network/networkInterfaces"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_nics { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_nics_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_nsgs_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Network/networkSecurityGroups"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_nsgs { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_nsgs_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_publicips_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Network/publicIPAddresses"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_publicips { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_publicips_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_route_tables_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Network/routeTables"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_route_tables { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_route_tables_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_snapshots_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Compute/snapshots"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_snapshots { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_snapshots_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_sqlvms_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.SqlVirtualMachine/sqlVirtualMachines"; + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "?api-version=" . $options{force_api_version} : "?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_sqlvms { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_sqlvms_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + +sub azure_list_sqlelasticpools_set_url { + my ($self, %options) = @_; + + my $url = $self->{management_endpoint} . "/subscriptions/" . $self->{subscription}; + $url .= "/resourceGroups/" . $options{resource_group} if (defined($options{resource_group}) && $options{resource_group} ne ''); + $url .= "/providers/Microsoft.Sql/servers/" . $options{server} if (defined($options{server}) && $options{server} ne ''); + $url .= (defined($options{force_api_version}) && $options{force_api_version} ne '') ? "/elasticPools?api-version=" . $options{force_api_version} : "/elasticPools?api-version=" . $self->{api_version}; + return $url; +} + +sub azure_list_sqlelasticpools { + my ($self, %options) = @_; + + my $full_response = []; + my $full_url = $self->azure_list_sqlelasticpools_set_url(%options); + while (1) { + my $response = $self->request_api(method => 'GET', full_url => $full_url, hostname => ''); + foreach (@{$response->{value}}) { + push @$full_response, $_; + } + + last if (!defined($response->{nextLink})); + $full_url = $response->{nextLink}; + } + + return $full_response; +} + 1; __END__ diff --git a/centreon-plugins/cloud/azure/management/costs/mode/hybridbenefits.pm b/centreon-plugins/cloud/azure/management/costs/mode/hybridbenefits.pm new file mode 100644 index 000000000..51ce935fa --- /dev/null +++ b/centreon-plugins/cloud/azure/management/costs/mode/hybridbenefits.pm @@ -0,0 +1,319 @@ +# +# 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::hybridbenefits; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; + +sub custom_orphaned_output { + my ($self, %options) = @_; + + my $msg = sprintf(" resources without hybrid benefits %s (out of: %s)", $self->{result_values}->{count}, $self->{result_values}->{total}); + + return $msg; +} + +sub prefix_vm_output { + my ($self, %options) = @_; + + return 'Virtual machines'; +} + +sub prefix_sql_vm_output { + my ($self, %options) = @_; + + return 'SQL Virtual machines'; +} + +sub prefix_sql_db_output { + my ($self, %options) = @_; + + return 'SQL Databases'; +} + +sub prefix_elasticpool_output { + my ($self, %options) = @_; + + return 'SQL Elastic Pools'; +} + +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 => { + 'skip-vm' => { name => 'skip_vm' }, + 'skip-sql-vm' => { name => 'skip_sql_vm' }, + 'skip-sql-database' => { name => 'skip_sql_database' }, + 'skip-elastic-pool' => { name => 'skip_elastic_pool' }, + 'exclude-name:s' => { name => 'exclude_name' } + }); + + return $self; +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'nohybridbenefits_resources', type => 0 }, + { name => 'nohybridbenefits_vm', type => 0, cb_prefix_output => 'prefix_vm_output' }, + { name => 'nohybridbenefits_sql_vm', type => 0, cb_prefix_output => 'prefix_sql_vm_output' }, + { name => 'nohybridbenefits_sql_db', type => 0, cb_prefix_output => 'prefix_sql_db_output' }, + { name => 'nohybridbenefits_elasticpool', type => 0, cb_prefix_output => 'prefix_elasticpool_output' } + ]; + +# vm, sql-vm, sql-database, elastic-pool + $self->{maps_counters}->{nohybridbenefits_resources} = [ + { label => 'resources', nlabel => 'azure.resources.nohybridbenefits.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + output_template => 'Resources without hybrid benefits: %s', + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{nohybridbenefits_vm} = [ + { label => 'vm', display_ok => 0, nlabel => 'azure.vm.nohybridbenefits.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_hybridbenefits_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{nohybridbenefits_sql_vm} = [ + { label => 'sql-vm', display_ok => 0, nlabel => 'azure.sqlvm.nohybridbenefits.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_hybridbenefits_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{nohybridbenefits_sql_db} = [ + { label => 'sql-database', display_ok => 0, nlabel => 'azure.sqldatabase.nohybridbenefits.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_hybridbenefits_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{nohybridbenefits_elasticpool} = [ + { label => 'elastic-pool', display_ok => 0, nlabel => 'azure.elasticpool.nohybridbenefits.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_hybridbenefits_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $resultset; + my @item_list; + $self->{nohybridbenefits_resources}->{count} = 0; + $self->{nohybridbenefits_resources}->{total} = 0; + + # VMs + if (!defined($self->{option_results}->{skip_vm})) { + $self->{nohybridbenefits_vm}->{count} = 0; + $self->{nohybridbenefits_vm}->{total} = 0; + $resultset = $options{custom}->azure_list_vms( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-03-01" + ); + + foreach my $item (@{ $resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{vm_hybrid_benefits}->{total}++; + $self->{nohybridbenefits_resources}->{total}++; + next if (!defined($item->{properties}->{licenseType}) || (defined($item->{properties}->{licenseType}) && $item->{properties}->{licenseType} !~ /None/)); + $self->{nohybridbenefits_vm}->{count}++; + $self->{nohybridbenefits_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "Virtual Machines withtout hybrid benefits:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + + # SQL VMs + if (!defined($self->{option_results}->{skip_sql_vm})) { + $self->{nohybridbenefits_sql_vm}->{count} = 0; + $self->{nohybridbenefits_sql_vm}->{total} = 0; + $resultset = $options{custom}->azure_list_sqlvms( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-02-01" + ); + foreach my $item (@{ $resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{nohybridbenefits_sql_vm}->{total}++; + $self->{nohybridbenefits_resources}->{total}++; + next if ($item->{properties}->{sqlServerLicenseType } =~ /AHUB/ || $item->{properties}->{sqlImageSku} =~ /Express/); + $self->{nohybridbenefits_sql_vm}->{count}++; + $self->{nohybridbenefits_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "SQL Virtual Machines withtout hybrid benefits:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + + } + + if (!defined($self->{option_results}->{skip_sql_vm}) || !defined($self->{option_results}->{skip_elastic_pool})) { + my @item_list_sql; + my @item_list_elastic; + $resultset = $options{custom}->azure_list_sqlservers( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2021-11-01" + ); + + $self->{nohybridbenefits_elasticpool}->{count} = 0; + $self->{nohybridbenefits_elasticpool}->{total} = 0; + $self->{nohybridbenefits_sql_db}->{count} = 0; + $self->{nohybridbenefits_sql_db}->{total} = 0; + + foreach my $item (@{$resultset}) { + my @sqlserver_id = split /\//, $item->{id}; + + # SQL databases + if (!defined($self->{option_results}->{skip_sql_database})) { + my $resultset_sql = $options{custom}->azure_list_sqldatabases( + resource_group => $sqlserver_id[4], + server => $item->{name}, + force_api_version => "2021-11-01" + ); + + foreach my $item_sql (@{ $resultset_sql}) { + next if ($item_sql->{properties}->{currentSku}->{name} =~ /ElasticPool/); + next if (defined($item_sql->{properties}->{currentSku}->{tier}) && $item_sql->{properties}->{currentSku}->{tier} !~ /GeneralPurpose/); + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item_sql->{name} =~ /$self->{option_results}->{exclude_name}/); + next if (defined($item_sql->{properties}->{licenseType}) && $item_sql->{properties}->{licenseType} eq "BasePrice"); + $self->{nohybridbenefits_sql_db}->{total}++; + $self->{nohybridbenefits_resources}->{total}++; + + if (defined($item_sql->{properties}->{licenseType})) { + $self->{nohybridbenefits_sql_db}->{count}++; + $self->{nohybridbenefits_resources}->{count}++; + push @item_list_sql, $item_sql->{name}; + } + } + } + + # SQL Elastic pools + if (!defined($self->{option_results}->{skip_elastic_pool})) { + my $resultset_elastic = $options{custom}->azure_list_sqlelasticpools( + resource_group => $sqlserver_id[4], + server => $item->{name}, + force_api_version => "2021-11-01" + ); + + foreach my $item_ep (@{$resultset_elastic}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item_ep->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{nohybridbenefits_elasticpool}->{total}++; + $self->{nohybridbenefits_resources}->{total}++; + next if (defined($item_ep->{properties}->{licenseType}) && $item_ep->{properties}->{licenseType} =~ /BasePrice/); + $self->{nohybridbenefits_elasticpool}->{count}++; + $self->{nohybridbenefits_resources}->{count}++; + push @item_list_elastic, $item_ep->{name}; + } + } + } + if (scalar @item_list_sql != 0) { + $self->{output}->output_add(long_msg => "SQL Databases withtout hybrid benefits:" . "[" . join(", ", @item_list_sql) . "]"); + } + if (scalar @item_list_elastic != 0) { + $self->{output}->output_add(long_msg => "SQL Elastic pools withtout hybrid benefits:" . "[" . join(", ", @item_list_elastic) . "]"); + } + } +} + +1; + +__END__ + +=head1 MODE + +Check if hybrid benefits is enabled on eligible resources. + +Example: +perl centreon_plugins.pl --plugin=cloud::azure::management::costs::plugin --custommode=api --mode=hybrid-benefits +{--resource-group='MYRESOURCEGROUP'] --exclude-name='MyDb|MyEpool.*' [--skip-vm] [--skip-sql-vm] [--skip-sql-database] [--skip-sql-elastic-pool] [--show-details --verbose] + +Adding --verbose will display the item names. + +=over 8 + +=item B<--resource-group> + +Set resource group. + +=item B<--exclude-name> + +Exclude resource from check (Can be a regexp). + +=item B<--warning-*> + +Warning threshold on the number of orphaned resources. +Substitue '*' by the resource type amongst this list: + ( elastic-pool sql-database vm sql-vm resources) + +=îtem B<--critical-*> + +Critical threshold on the number of orphaned resources. +Substitue '*' by the resource type amongst this list: + (elastic-pool sql-database vm sql-vm resources) + +=item B<--skip-*> + +Skip a specific kind of resource. Can be multiple. + +Accepted values: vm, sql-vm, sql-database, elastic-pool + +=back + +=back + +=cut diff --git a/centreon-plugins/cloud/azure/management/costs/mode/orphanresources.pm b/centreon-plugins/cloud/azure/management/costs/mode/orphanresources.pm new file mode 100644 index 000000000..6877803a8 --- /dev/null +++ b/centreon-plugins/cloud/azure/management/costs/mode/orphanresources.pm @@ -0,0 +1,385 @@ +# +# 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::orphanresources; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; + +sub custom_orphaned_output { + my ($self, %options) = @_; + + my $msg = sprintf(" orphaned resources %s (total: %s)", $self->{result_values}->{count}, $self->{result_values}->{total}); + + return $msg; +} + +sub prefix_disks_output { + my ($self, %options) = @_; + + return 'Managed disks'; +} + +sub prefix_nsgs_output { + my ($self, %options) = @_; + + return 'NSGs'; +} + +sub prefix_nics_output { + my ($self, %options) = @_; + + return 'NICs'; +} + +sub prefix_publicips_output { + my ($self, %options) = @_; + + return 'Public IPs'; +} + +sub prefix_routetables_output { + my ($self, %options) = @_; + + return 'Route tables'; +} + +sub prefix_snapshots_output { + my ($self, %options) = @_; + + return 'Snapshots'; +} + +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 => { + 'exclude-name:s' => { name => 'exclude_name' }, + 'skip-managed-disks' => { name => 'skip_managed_disks' }, + 'skip-nics' => { name => 'skip_nics' }, + 'skip-nsgs' => { name => 'skip_nsgs' }, + 'skip-public-ips' => { name => 'skip_public_ips' }, + 'skip-route-tables' => { name => 'skip_route_tables' }, + 'skip-snapshots' => { name => 'skip_snapshots' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'orphaned_resources', type => 0 }, + { name => 'orphaned_disks', type => 0, cb_prefix_output => 'prefix_disks_output' }, + { name => 'orphaned_nics', type => 0, cb_prefix_output => 'prefix_nics_output' }, + { name => 'orphaned_nsgs', type => 0, cb_prefix_output => 'prefix_nsgs_output' }, + { name => 'orphaned_publicips', type => 0, cb_prefix_output => 'prefix_publicips_output' }, + { name => 'orphaned_routetables', type => 0, cb_prefix_output => 'prefix_routetables_output' }, + { name => 'orphaned_snapshots', type => 0, cb_prefix_output => 'prefix_snapshots_output' } + ]; + +# nics, nsgs, public-ips, route-tables, snapshots + $self->{maps_counters}->{orphaned_resources} = [ + { label => 'orphaned-resources', nlabel => 'azure.resources.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total'} ], + output_template => 'Orphaned resources: %s', + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{orphaned_disks} = [ + { label => 'orphaned-managed-disks', display_ok => 0, nlabel => 'azure.manageddisks.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_orphaned_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{orphaned_nics} = [ + { label => 'orphaned-nics', display_ok => 0, nlabel => 'azure.nics.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_orphaned_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{orphaned_nsgs} = [ + { label => 'orphaned-nsgs', display_ok => 0, nlabel => 'azure.nsgs.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_orphaned_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{orphaned_publicips} = [ + { label => 'orphaned-publicips', display_ok => 0, nlabel => 'azure.publicips.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_orphaned_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{orphaned_routetables} = [ + { label => 'orphaned-routetables', display_ok => 0, nlabel => 'azure.routetables.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_orphaned_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + $self->{maps_counters}->{orphaned_snapshots} = [ + { label => 'orphaned-snapshots', display_ok => 0, nlabel => 'azure.snapshots.orphaned.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_orphaned_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total'} + ] + } + } + ]; +} + +sub manage_selection { + my ($self, %options) = @_; + + my $resultset; + my @item_list; + $self->{orphaned_resources}->{count} = 0; + $self->{orphaned_resources}->{total} = 0; + + # orphan managed disks + if (!defined($self->{option_results}->{skip_managed_disks})) { + $self->{orphaned_disks}->{count} = 0; + $self->{orphaned_disks}->{total} = 0; + $resultset = $options{custom}->azure_list_compute_disks( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-07-02" + ); + foreach my $item (@{$resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{orphaned_disks}->{total}++; + $self->{orphaned_resources}->{total}++; + next if ($item->{properties}->{diskState} !~ /Unattached/); + $self->{orphaned_disks}->{count}++; + $self->{orphaned_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "Managed Disks orphaned list:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + + # orphan NICs + if (!defined($self->{option_results}->{skip_nics})) { + $self->{orphaned_nics}->{count} = 0; + $self->{orphaned_nics}->{total} = 0; + $resultset = $options{custom}->azure_list_nics( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-05-01" + ); + foreach my $item (@{$resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{orphaned_nics}->{total}++; + $self->{orphaned_resources}->{total}++; + next if (scalar(keys %{$item->{properties}->{virtualMachine}}) != 0 || defined($item->{properties}->{privateEndpoint})); + $self->{orphaned_nics}->{count}++; + $self->{orphaned_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "NICs orphaned list:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + + # orphan NSGs + if (!defined($self->{option_results}->{skip_nsgs})) { + $self->{orphaned_nsgs}->{count} = 0; + $self->{orphaned_nsgs}->{total} = 0; + + $resultset = $options{custom}->azure_list_nsgs( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-05-01" + ); + foreach my $item (@{$resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{orphaned_nsgs}->{total}++; + $self->{orphaned_resources}->{total}++; + next if (defined($item->{properties}->{subnets}) && scalar($item->{properties}->{subnets}) != 0); + next if (defined($item->{properties}->{networkInterface}) && scalar($item->{properties}->{networkInterface}) != 0); + next if (defined($item->{properties}->{networkInterfaces}) && scalar($item->{properties}->{networkInterfaces}) != 0); + $self->{orphaned_nsgs}->{count}++; + $self->{orphaned_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "NSGs orphaned list:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + + # orphan public IPs + if (!defined($self->{option_results}->{skip_public_ips})) { + $self->{orphaned_publicips}->{count} = 0; + $self->{orphaned_publicips}->{total} = 0; + $resultset = $options{custom}->azure_list_publicips( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-01-01" + ); + foreach my $item (@{$resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{orphaned_publicips}->{total}++; + $self->{orphaned_resources}->{total}++; + next if (defined($item->{properties}->{ipConfiguration}) && scalar($item->{properties}->{ipConfiguration}) != 0); + $self->{orphaned_publicips}->{count}++; + $self->{orphaned_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "Public IPs orphaned list:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + + + + # orphan route tables + if (!defined($self->{option_results}->{skip_route_tables})) { + $self->{orphaned_routetables}->{count} = 0; + $self->{orphaned_routetables}->{total} = 0; + $resultset = $options{custom}->azure_list_route_tables( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-01-01" + ); + foreach my $item (@{$resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{orphaned_routetables}->{total}++; + $self->{orphaned_resources}->{total}++; + next if (defined($item->{properties}->{subnets}) && scalar($item->{properties}->{subnets}) != 0); + $self->{orphaned_routetables}->{count}++; + $self->{orphaned_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "Route tables orphaned list:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + + + # orphan snapshots + if (!defined($self->{option_results}->{skip_snapshots})) { + $self->{orphaned_snapshots}->{count} = 0; + $self->{orphaned_snapshots}->{total} = 0; + $resultset = $options{custom}->azure_list_snapshots( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2021-12-01" + ); + foreach my $item (@{$resultset}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{orphaned_snapshots}->{total}++; + $self->{orphaned_resources}->{total}++; + next if (defined($item->{properties}->{subnets}) && scalar($item->{properties}->{subnets}) != 0); + $self->{orphaned_snapshots}->{count}++; + $self->{orphaned_resources}->{count}++; + push @item_list, $item->{name}; + } + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "Snapshots orphaned list:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + +} + +1; + +__END__ + +=head1 MODE + +Check orphaned resource within an Azure subscription. + +Example: +perl centreon_plugins.pl --plugin=cloud::azure::management::costs::plugin --custommode=api --mode=orphan-resource +{--resource-group='MYRESOURCEGROUP'] --exclude-name='MyDisk|DataDisk.*' [--skip-managed-disks] [--skip-nics] [--skip-nsgs] [--skip-public-ips] [--skip-route-tables] [--skip-snapshots] + +Adding --verbose will display the item names. + +=over 8 + +=item B<--resource-group> + +Set resource group. + +=item B<--exclude-name> + +Exclude resource from check (Can be a regexp). + +=item B<--warning-*> + +Warning threshold on the number of orphaned resources. +Substitue '*' by the resource type amongst this list: + (orphaned-snapshots orphaned-routetables orphaned-managed-disks orphaned-nsgs orphaned-nics orphaned-resources orphaned-publicips) + +=îtem B<--critical-*> + +Critical threshold on the number of orphaned resources. +Substitue '*' by the resource type amongst this list: + (orphaned-snapshots orphaned-routetables orphaned-managed-disks orphaned-nsgs orphaned-nics orphaned-resources orphaned-publicips) + +=item B<--skip-*> + +Skip a specific kind of resource. Can be multiple. + +Accepted values: disks, nics, nsgs, public-ips, route-tables, snapshots + +=back + +=cut \ No newline at end of file diff --git a/centreon-plugins/cloud/azure/management/costs/mode/tagscompliance.pm b/centreon-plugins/cloud/azure/management/costs/mode/tagscompliance.pm new file mode 100644 index 000000000..85cbf86f2 --- /dev/null +++ b/centreon-plugins/cloud/azure/management/costs/mode/tagscompliance.pm @@ -0,0 +1,214 @@ +# +# 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::tagscompliance; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; + +sub prefix_vm_output { + my ($self, %options) = @_; + + return "Virtual Machines"; +} + +sub custom_compliance_output { + my ($self, %options) = @_; + + my $msg = sprintf(" not having specified tags %s (out of %s)", $self->{result_values}->{count}, $self->{result_values}->{total}); + + return $msg; +} + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options, force_new_perdata => 1); + bless $self, $class; + + $options{options}->add_options(arguments => { + 'skip_vm' => { name => 'skip_vm' }, + 'tags:s@' => { name => 'tags' }, + 'exclude-name:s' => { name => 'exclude_name' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + if (!defined($self->{option_results}->{tags}) || $self->{option_results}->{tags} eq '') { + $self->{output}->add_option_msg(short_msg => "Need to specify --tags option"); + $self->{output}->option_exit(); + } + + foreach my $tag_pair (@{$self->{option_results}->{tags}}) { + my ($key, $value) = split / => /, $tag_pair; + centreon::plugins::misc::trim($value) if defined($value); + if (!exists($self->{tags}->{ centreon::plugins::misc::trim($key) })) { + $self->{tags}->{ centreon::plugins::misc::trim($key) } = $value; + } else { + $self->{output}->add_option_msg(short_msg => "Using multiple --tags option with the same key is forbiden. Please use regexp on the value instead"); + $self->{output}->option_exit(); + } + + } +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'uncompliant_resource', type => 0 }, + { name => 'uncompliant_vms', type => 0 }, + ]; + + $self->{maps_counters}->{uncompliant_resource} = [ + { label => 'uncompliant-resource', nlabel => 'azure.tags.resource.notcompliant.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + output_template => 'Uncompliant resources: %s', + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; + + $self->{maps_counters}->{uncompliant_vms} = [ + { label => 'uncompliant-vms', display_ok => 0, nlabel => 'azure.tags.vm.notcompliant.count', set => { + key_values => [ { name => 'count' }, { name => 'total' } ], + closure_custom_output => $self->can('custom_compliance_output'), + perfdatas => [ + { template => '%d', min => 0, max => 'total' } + ] + } + } + ]; +} + +sub manage_selection { + my ($self, %options) = @_; + + my @item_list; + $self->{uncompliant_resource}->{count} = 0; + $self->{uncompliant_resource}->{total} = 0; + + if (!defined($self->{option_results}->{skip_vm})) { + $self->{uncompliant_vms}->{count} = 0; + $self->{uncompliant_vms}->{total} = 0; + my $items = $options{custom}->azure_list_vms( + resource_group => $self->{option_results}->{resource_group}, + force_api_version => "2022-08-01" + ); + + foreach my $item (@{$items}) { + next if (defined($self->{option_results}->{exclude_name}) && $self->{option_results}->{exclude_name} ne '' + && $item->{name} =~ /$self->{option_results}->{exclude_name}/); + $self->{uncompliant_vms}->{total}++; + $self->{uncompliant_resource}->{total}++; + + my $matched = "0"; + foreach my $lookup_key (keys %{ $self->{tags} }) { + foreach my $vm_key (keys %{ $item->{tags} }) { + if (defined($self->{tags}->{$lookup_key}) && defined($item->{tags}->{$vm_key})) { + $matched++ if ($item->{tags}->{$vm_key} =~ /$self->{tags}->{$lookup_key}/); + } + if (!defined($self->{tags}->{$lookup_key})) { + $matched++ if ($vm_key eq $lookup_key); + } + } + } + if (scalar keys %{ $self->{tags} } != $matched) { + $self->{uncompliant_vms}->{count}++; + $self->{uncompliant_resource}->{count}++; + push @item_list, $item->{name}; + } + } + + if (scalar @item_list != 0) { + $self->{output}->output_add(long_msg => "Virtual Machines with uncompliant tags:" . "[" . join(", ", @item_list) . "]"); + } + @item_list = (); + } + +} + +1; + +__END__ + +=head1 MODE + +Check if a specified tag is present on your Azure resources. + +At the moment, only VMs are supported, but support will extend to other resource type in the future. + +Example: +perl centreon_plugins.pl --plugin=cloud::azure::management::costs::plugin --custommode=api --mode=tag-on-resources +{--resource-group='MYRESOURCEGROUP'] --exclude-name='MyVM1|MyVM2.*' --tag-name='atagname' --tag-name='atagname => atagvalue' --api-version='2022-08-01' + +Adding --verbose will display the item names. + +=over 8 + +=item B<--resource-group> + +Set resource group (Optional). + +=item B<--exclude-name> + +Exclude resource from check (Can be a regexp). + +=item B<--skip-vm> + +Skip virtual machines (don't use it until other resource type are supported) + +=item B<--tags> + +Can be multiple. Allow you to specify tags that should be present. All tags must match a resource's configuration +to make it a compliant one. + +What you cannot do: + +- specifying the same key in different options: --tags='Environment => Prod' --tags='Environment => Dev' + +What you can do: +- check for multiple value for a single key: --tags='Environment => Dev|Prod' +- check for a key, without minding about its value: --tags='Version' +- combine the two: --tags='Environment => Dev|Prod' --tags='Version' + +=item B<--warning-*> + +Warning threshold. '*' replacement values accepted: +- uncompliant-vms +- uncompliant-resource + +=item B<--critical-*> + +Critical threshold. '*' replacement values accepted: +- uncompliant-vms +- uncompliant-resource + +=back + +=cut diff --git a/centreon-plugins/cloud/azure/management/costs/plugin.pm b/centreon-plugins/cloud/azure/management/costs/plugin.pm index a2af428dc..1ff60691c 100644 --- a/centreon-plugins/cloud/azure/management/costs/plugin.pm +++ b/centreon-plugins/cloud/azure/management/costs/plugin.pm @@ -31,9 +31,12 @@ sub new { $self->{version} = '0.1'; %{ $self->{modes} } = ( - 'budgets' => 'cloud::azure::management::costs::mode::budgets', - 'costs-explorer' => 'cloud::azure::management::costs::mode::costsexplorer', - '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', + 'orphan-resources' => 'cloud::azure::management::costs::mode::orphanresources', + 'hybrid-benefits' => 'cloud::azure::management::costs::mode::hybridbenefits', + 'tags-compliance' => 'cloud::azure::management::costs::mode::tagscompliance' ); $self->{custom_modes}{api} = 'cloud::azure::custom::api';