From 3bc2da30c7797f4158c2042ce0daaacbd8e53c06 Mon Sep 17 00:00:00 2001 From: garnier-quentin Date: Thu, 4 Jun 2020 11:37:47 +0200 Subject: [PATCH] wip versa director --- network/versa/director/restapi/custom/api.pm | 365 ++++++++++++++++++ .../versa/director/restapi/mode/devices.pm | 338 ++++++++++++++++ network/versa/director/restapi/plugin.pm | 49 +++ 3 files changed, 752 insertions(+) create mode 100644 network/versa/director/restapi/custom/api.pm create mode 100644 network/versa/director/restapi/mode/devices.pm create mode 100644 network/versa/director/restapi/plugin.pm diff --git a/network/versa/director/restapi/custom/api.pm b/network/versa/director/restapi/custom/api.pm new file mode 100644 index 000000000..250d7fc78 --- /dev/null +++ b/network/versa/director/restapi/custom/api.pm @@ -0,0 +1,365 @@ +# +# Copyright 2020 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 network::versa::director::restapi::custom::api; + +use strict; +use warnings; +use centreon::plugins::http; +use centreon::plugins::statefile; +use JSON::XS; +use Digest::MD5 qw(md5_hex); + +sub new { + my ($class, %options) = @_; + my $self = {}; + bless $self, $class; + + if (!defined($options{output})) { + print "Class Custom: Need to specify 'output' argument.\n"; + exit 3; + } + if (!defined($options{options})) { + $options{output}->add_option_msg(short_msg => "Class Custom: Need to specify 'options' argument."); + $options{output}->option_exit(); + } + + if (!defined($options{noptions})) { + $options{options}->add_options(arguments => { + 'hostname:s' => { name => 'hostname' }, + 'port:s' => { name => 'port' }, + 'proto:s' => { name => 'proto' }, + 'api-username:s' => { name => 'api_username' }, + 'api-password:s' => { name => 'api_password' }, + 'timeout:s' => { name => 'timeout' }, + 'reload-cache-time:s' => { name => 'reload_cache_time' }, + 'ignore-unknown-errors' => { name => 'ignore_unknown_errors' }, + 'unknown-http-status:s' => { name => 'unknown_http_status' }, + 'warning-http-status:s' => { name => 'warning_http_status' }, + 'critical-http-status:s' => { name => 'critical_http_status' } + }); + } + $options{options}->add_help(package => __PACKAGE__, sections => 'REST API OPTIONS', once => 1); + + $self->{output} = $options{output}; + $self->{mode} = $options{mode}; + $self->{http} = centreon::plugins::http->new(%options); + $self->{cache} = centreon::plugins::statefile->new(%options); + $self->{cache_checked} = 0; + + return $self; +} + +sub set_options { + my ($self, %options) = @_; + + $self->{option_results} = $options{option_results}; +} + +sub set_defaults { + my ($self, %options) = @_; + + foreach (keys %{$options{default}}) { + if ($_ eq $self->{mode}) { + for (my $i = 0; $i < scalar(@{$options{default}->{$_}}); $i++) { + foreach my $opt (keys %{$options{default}->{$_}[$i]}) { + if (!defined($self->{option_results}->{$opt}[$i])) { + $self->{option_results}->{$opt}[$i] = $options{default}->{$_}[$i]->{$opt}; + } + } + } + } + } +} + +sub check_options { + my ($self, %options) = @_; + + $self->{hostname} = (defined($self->{option_results}->{hostname})) ? $self->{option_results}->{hostname} : ''; + $self->{port} = (defined($self->{option_results}->{port})) ? $self->{option_results}->{port} : 9182; + $self->{proto} = (defined($self->{option_results}->{proto})) ? $self->{option_results}->{proto} : 'https'; + $self->{timeout} = (defined($self->{option_results}->{timeout})) ? $self->{option_results}->{timeout} : 10; + $self->{api_username} = (defined($self->{option_results}->{api_username})) ? $self->{option_results}->{api_username} : ''; + $self->{api_password} = (defined($self->{option_results}->{api_password})) ? $self->{option_results}->{api_password} : ''; + $self->{reload_cache_time} = (defined($self->{option_results}->{reload_cache_time})) ? $self->{option_results}->{reload_cache_time} : 180; + $self->{ignore_unknown_errors} = (defined($self->{option_results}->{ignore_unknown_errors})) ? 1 : 0; + + my $default_unknown = '(%{http_code} < 200 or %{http_code} >= 300)'; + if ($self->{ignore_unknown_errors} == 1) { + $default_unknown = '(%{http_code} < 200 or %{http_code} >= 300) and %{http_code} != 404'; + } + $self->{unknown_http_status} = (defined($self->{option_results}->{unknown_http_status})) ? $self->{option_results}->{unknown_http_status} : $default_unknown; + $self->{warning_http_status} = (defined($self->{option_results}->{warning_http_status})) ? $self->{option_results}->{warning_http_status} : ''; + $self->{critical_http_status} = (defined($self->{option_results}->{critical_http_status})) ? $self->{option_results}->{critical_http_status} : ''; + + if ($self->{hostname} eq '') { + $self->{output}->add_option_msg(short_msg => "Need to specify --hostname option."); + $self->{output}->option_exit(); + } + if ($self->{api_username} eq '') { + $self->{output}->add_option_msg(short_msg => "Need to specify --api-username option."); + $self->{output}->option_exit(); + } + if ($self->{api_password} eq '') { + $self->{output}->add_option_msg(short_msg => "Need to specify --api-password option."); + $self->{output}->option_exit(); + } + + $self->{cache}->check_options(option_results => $self->{option_results}); + return 0; +} + +sub get_hostname { + my ($self, %options) = @_; + + return $self->{hostname}; +} + +sub get_cache_organizations { + my ($self, %options) = @_; + + $self->cache_meraki_entities(); + return $self->{cache_organizations}; +} + +sub get_cache_networks { + my ($self, %options) = @_; + + $self->cache_meraki_entities(); + return $self->{cache_networks}; +} + +sub get_cache_devices { + my ($self, %options) = @_; + + $self->cache_meraki_entities(); + return $self->{cache_devices}; +} + +sub build_options_for_httplib { + my ($self, %options) = @_; + + $self->{option_results}->{hostname} = $self->{hostname}; + $self->{option_results}->{timeout} = $self->{timeout}; + $self->{option_results}->{port} = $self->{port}; + $self->{option_results}->{proto} = $self->{proto}; + $self->{option_results}->{credentials} = 1; + $self->{option_results}->{basic} = 1; + $self->{option_results}->{username} = $self->{api_username}; + $self->{option_results}->{password} = $self->{api_password}; +} + +sub settings { + my ($self, %options) = @_; + + return if (defined($self->{settings_done})); + $self->build_options_for_httplib(); + $self->{http}->set_options(%{$self->{option_results}}); + $self->{settings_done} = 1; +} + +sub bouchon { + my ($self, %options) = @_; + + my $content = do { + local $/ = undef; + if (!open my $fh, "<", $options{file}) { + $self->{output}->add_option_msg(short_msg => "Could not open file $options{file} : $!"); + $self->{output}->option_exit(); + } + <$fh>; + }; + + eval { + $content = JSON::XS->new->allow_nonref(1)->utf8->decode($content); + }; + if ($@) { + $self->{output}->add_option_msg(short_msg => "Cannot decode json response: $@"); + $self->{output}->option_exit(); + } + + return $content; +} + +sub request_api { + my ($self, %options) = @_; + + $self->settings(); + my $response = $self->{http}->request( + url_path => $options{endpoint}, + critical_status => $self->{critical_http_status}, + warning_status => $self->{warning_http_status}, + unknown_status => $self->{unknown_http_status} + ); + + my $code = $self->{http}->get_code(); + return [] if ($code == 404 && $self->{ignore_unknown_errors} == 1); + + my $content; + eval { + $content = JSON::XS->new->allow_nonref(1)->utf8->decode($response); + }; + if ($@) { + $self->{output}->add_option_msg(short_msg => "Cannot decode json response: $@"); + $self->{output}->option_exit(); + } + + return ($content); +} + +sub cache_versa_entities { + my ($self, %options) = @_; + + return if ($self->{cache_checked} == 1); + + $self->{cache_checked} = 1; + my $has_cache_file = $self->{cache}->read(statefile => 'cache_versa_' . $self->get_hostname()); + my $timestamp_cache = $self->{cache}->get(name => 'last_timestamp'); + $self->{cache_organizations} = $self->{cache}->get(name => 'organizations'); + $self->{cache_appliances} = $self->{cache}->get(name => 'appliances'); + + if ($has_cache_file == 0 || !defined($timestamp_cache) || ((time() - $timestamp_cache) > (($self->{reload_cache_time}) * 60))) { + $self->{cache_organizations} = $self->get_organizations( + disable_cache => 1 + ); + $self->{cache_appliances} = $self->get_appliances( + disable_cache => 1 + ); + + $self->{cache}->write(data => { + last_timestamp => time(), + organizations => $self->{cache_organizations}, + appliances => $self->{cache_appliances} + }); + } +} + +sub get_organizations { + my ($self, %options) = @_; + + my $datas = $self->bouchon(file => '/home/qgarnier/clients/plugins/todo/versa/Versa-Centreon/organizations.json'); + + $self->cache_versa_entities(); + return $self->{cache_organizations} if (!defined($options{disable_cache}) || $options{disable_cache} == 0); + #my $datas = $self->request_api(endpoint => '/api/config/nms/provider/organizations/organization'); + + my $results = { entries => {}, names => { } }; + + if (defined($datas->{organization})) { + foreach (@{$datas->{organization}}) { + $results->{entries}->{ $_->{uuid} } = $_; + $results->{names}->{ $_->{name} } = $_->{uuid}; + } + } + + return $results; +} + +sub get_appliances { + my ($self, %options) = @_; + + my $datas = $self->bouchon(file => '/home/qgarnier/clients/plugins/todo/versa/Versa-Centreon/appliances.json'); + + $self->cache_versa_entities(); + return $self->{cache_appliances} if (!defined($options{disable_cache}) || $options{disable_cache} == 0); + #my $datas = $self->request_api(endpoint => '/api/config/nms/provider/appliances/appliance'); + + my $results = { entries => {}, names => { }, types => { } }; + + if (defined($datas->{appliance})) { + foreach (@{$datas->{appliance}}) { + $results->{entries}->{ $_->{uuid} } = $_; + $results->{names}->{ $_->{name} } = $_->{uuid}; + $results->{types}->{ $_->{type} } = {} if (!defined($results->{types}->{ $_->{type} })); + $results->{types}->{ $_->{type} }->{ $_->{name} } = $_->{uuid}; + } + } + + return $results; +} + +sub execute { + my ($self, %options) = @_; + + return $self->bouchon(file => '/home/qgarnier/clients/plugins/todo/versa/Versa-Centreon/appliance_status.json'); + + $self->cache_versa_entities(); + my $results = $self->request_api( + endpoint => $options{endpoint}, + ); + + return $results; +} + +1; + +__END__ + +=head1 NAME + +Versa Director REST API + +=head1 SYNOPSIS + +Rest API custom mode + +=head1 REST API OPTIONS + +=over 8 + +=item B<--hostname> + +Director hostname (Required) + +=item B<--port> + +Port used (Default: 9182) + +=item B<--proto> + +Specify https if needed (Default: 'https') + +=item B<--api-username> + +Versa Director API username. + +=item B<--api-password> + +Versa Director API password. + +=item B<--timeout> + +Set HTTP timeout + +=item B<--reload-cache-time> + +Time in minutes before reloading cache file (default: 180). + +=item B<--ignore-unknown-errors> + +Ignore unknown errors (404 status code). + +=back + +=head1 DESCRIPTION + +B. + +=cut diff --git a/network/versa/director/restapi/mode/devices.pm b/network/versa/director/restapi/mode/devices.pm new file mode 100644 index 000000000..ffeb57c24 --- /dev/null +++ b/network/versa/director/restapi/mode/devices.pm @@ -0,0 +1,338 @@ +# +# Copyright 2020 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 network::versa::director::restapi::mode::devices; + +use base qw(centreon::plugins::templates::counter); + +use strict; +use warnings; +use centreon::plugins::templates::catalog_functions qw(catalog_status_threshold); +use Digest::MD5 qw(md5_hex); + +sub custom_status_output { + my ($self, %options) = @_; + + return 'status: ' . $self->{result_values}->{status}; +} + +sub custom_link_status_output { + my ($self, %options) = @_; + + return 'status: ' . $self->{result_values}->{link_status}; +} + +sub set_counters { + my ($self, %options) = @_; + + $self->{maps_counters_type} = [ + { name => 'global', type => 0, cb_prefix_output => 'prefix_global_output', skipped_code => { -10 => 1 } }, + { name => 'devices', type => 3, cb_prefix_output => 'prefix_device_output', cb_long_output => 'device_long_output', indent_long_output => ' ', message_multiple => 'All devices are ok', + group => [ + { name => 'device_status', type => 0, skipped_code => { -10 => 1 } }, + { name => 'device_connections', type => 0, cb_prefix_output => 'prefix_connection_output', skipped_code => { -10 => 1 } }, + { name => 'device_traffic', type => 0, cb_prefix_output => 'prefix_traffic_output', skipped_code => { -10 => 1, -11 => 1 } }, + { name => 'device_links', display_long => 1, cb_prefix_output => 'prefix_link_output', message_multiple => 'All links are ok', type => 1, skipped_code => { -10 => 1 } }, + ] + } + ]; + + $self->{maps_counters}->{global} = [ + { label => 'total', nlabel => 'devices.total.count', display_ok => 0, set => { + key_values => [ { name => 'total'} ], + output_template => 'total: %s', + perfdatas => [ + { template => '%s', min => 0, max => 'total' } + ] + } + } + ]; + + $self->{maps_counters}->{device_status} = [ + { label => 'status', threshold => 0, set => { + key_values => [ { name => 'status' }, { name => 'display' } ], + closure_custom_calc => \&catalog_status_calc, + closure_custom_output => $self->can('custom_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold + } + } + ]; + + $self->{maps_counters}->{device_connections} = [ + { label => 'connections-success', nlabel => 'device.connections.success.count', set => { + key_values => [ { name => 'assoc' }, { name => 'display' } ], + output_template => 'success: %s', + perfdatas => [ + { template => '%d', min => 0, label_extra_instance => 1, instance_use => 'display' } + ] + } + }, + { label => 'connections-auth', nlabel => 'device.connections.auth.count', display_ok => 0, set => { + key_values => [ { name => 'auth' }, { name => 'display' } ], + output_template => 'auth: %s', + perfdatas => [ + { template => '%d', min => 0, label_extra_instance => 1, instance_use => 'display' } + ] + } + }, + { label => 'connections-assoc', nlabel => 'device.connections.assoc.count', display_ok => 0, set => { + key_values => [ { name => 'assoc' }, { name => 'display' } ], + output_template => 'assoc: %s', + perfdatas => [ + { template => '%d', min => 0, label_extra_instance => 1, instance_use => 'display' } + ] + } + }, + { label => 'connections-dhcp', nlabel => 'device.connections.dhcp.count', display_ok => 0, set => { + key_values => [ { name => 'dhcp' }, { name => 'display' } ], + output_template => 'dhcp: %s', + perfdatas => [ + { template => '%d', min => 0, label_extra_instance => 1, instance_use => 'display' } + ] + } + }, + { label => 'connections-dns', nlabel => 'device.connections.dns.count', display_ok => 0, set => { + key_values => [ { name => 'dns' }, { name => 'display' } ], + output_template => 'dns: %s', + perfdatas => [ + { template => '%d', min => 0, label_extra_instance => 1, instance_use => 'display' } + ] + } + } + ]; + + $self->{maps_counters}->{device_traffic} = [ + { label => 'traffic-in', nlabel => 'device.traffic.in.bitspersecond', set => { + key_values => [ { name => 'traffic_in', per_second => 1 }, { name => 'display' } ], + output_template => 'in: %s %s/s', + output_change_bytes => 2, + perfdatas => [ + { template => '%s', min => 0, unit => 'b/s', label_extra_instance => 1, instance_use => 'display' } + ] + } + }, + { label => 'traffic-out', nlabel => 'device.traffic.out.bitspersecond', set => { + key_values => [ { name => 'traffic_out', per_second => 1 }, { name => 'display' } ], + output_template => 'out: %s %s/s', + output_change_bytes => 2, + perfdatas => [ + { template => '%s', min => 0, unit => 'b/s', label_extra_instance => 1, instance_use => 'display' } + ] + } + } + ]; + + $self->{maps_counters}->{device_links} = [ + { label => 'link-status', threshold => 0, set => { + key_values => [ { name => 'link_status' }, { name => 'display' } ], + closure_custom_calc => \&catalog_status_calc, + closure_custom_output => $self->can('custom_link_status_output'), + closure_custom_perfdata => sub { return 0; }, + closure_custom_threshold_check => \&catalog_status_threshold + } + } + ]; +} + +sub device_long_output { + my ($self, %options) = @_; + + return "checking device '" . $options{instance_value}->{display} . "'"; +} + +sub prefix_device_output { + my ($self, %options) = @_; + + return "Device '" . $options{instance_value}->{display} . "' "; +} + +sub prefix_global_output { + my ($self, %options) = @_; + + return 'Devices '; +} + +sub prefix_connection_output { + my ($self, %options) = @_; + + return 'connection '; +} + +sub prefix_traffic_output { + my ($self, %options) = @_; + + return 'traffic '; +} + +sub prefix_link_output { + my ($self, %options) = @_; + + return "link '" . $options{instance_value}->{display} . "' "; +} + +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 => { + 'filter-device-name:s' => { name => 'filter_device_name' }, + 'filter-device-type:s' => { name => 'filter_device_type' }, + 'filter-device-org-name:s' => { name => 'filter_device_org_name' }, + 'unknown-status:s' => { name => 'unknown_status', default => '' }, + 'warning-status:s' => { name => 'warning_status', default => '' }, + 'critical-status:s' => { name => 'critical_status', default => '%{status} =~ /alerting/i' }, + 'unknown-link-status:s' => { name => 'unknown_link_status', default => '' }, + 'warning-link-status:s' => { name => 'warning_link_status', default => '' }, + 'critical-link-status:s' => { name => 'critical_link_status', default => '%{link_status} =~ /failed/i' } + }); + + return $self; +} + +sub check_options { + my ($self, %options) = @_; + $self->SUPER::check_options(%options); + + $self->change_macros(macros => [ + 'unknown_status', 'warning_status', 'critical_status', + 'unknown_link_status', 'warning_link_status', 'critical_link_status' + ]); +} + +sub manage_selection { + my ($self, %options) = @_; + + my $organizations = $options{custom}->get_organizations(); + my $devices = $options{custom}->get_appliances(); + + $self->{global} = { total => 0 }; + $self->{devices} = {}; + foreach my $device (values %{$devices->{entries}}) { + if (defined($self->{option_results}->{filter_device_name}) && $self->{option_results}->{filter_device_name} ne '' && + $device->{name} !~ /$self->{option_results}->{filter_device_name}/) { + $self->{output}->output_add(long_msg => "skipping device '" . $device->{name} . "': no matching filter name.", debug => 1); + next; + } + if (defined($self->{option_results}->{filter_device_type}) && $self->{option_results}->{filter_device_type} ne '' && + $device->{type} !~ /$self->{option_results}->{filter_device_type}/) { + $self->{output}->output_add(long_msg => "skipping device '" . $device->{name} . "': no matching filter type.", debug => 1); + next; + } + if (defined($self->{option_results}->{filter_device_org_name}) && $self->{option_results}->{filter_device_org_name} ne '') { + my $matched = 0; + foreach (@{$device->{orgs}}) { + if ($organizations->{entries}->{ $_->{uuid} }->{name} =~ /$self->{option_results}->{filter_device_org_name}/) { + $matched = 1; + last; + } + } + if ($matched == 0) { + $self->{output}->output_add(long_msg => "skipping device '" . $device->{name} . "': no matching filter org.", debug => 1); + next; + } + } + + #"ping-status": "REACHABLE", + #"sync-status": "OUT_OF_SYNC", + #"services-status": "GOOD", + #"overall-status": "POWERED_ON", + #"controller-status": "Unavailable", + #"path-status": "Unavailable", + # "Hardware": { + # "memory": "7.80GiB", + # "freeMemory": "1.19GiB", + # "diskSize": "80G", + # "freeDisk": "33G", + #} + # need alarms also + my $appliance_status = $options{custom}->execute(endpoint => '/vnms/dashboard/applianceStatus/' . $device->{uuid}); + + $self->{global}->{total}++; + } + + if (scalar(keys %{$self->{devices}}) <= 0) { + $self->{output}->output_add(short_msg => 'no devices found'); + } +} + +1; + +__END__ + +=head1 MODE + +Check devices. + +=over 8 + +=item B<--filter-device-name> + +Filter device by name (Can be a regexp). + +=item B<--filter-device-type> + +Filter device by type (Can be a regexp). + +=item B<--filter-device-org-name> + +Filter device by organization name (Can be a regexp). + +=item B<--unknown-status> + +Set unknown threshold for status. +Can used special variables like: %{status}, %{display} + +=item B<--warning-status> + +Set warning threshold for status. +Can used special variables like: %{status}, %{display} + +=item B<--critical-status> + +Set critical threshold for status (Default: '%{status} =~ /alerting/i'). +Can used special variables like: %{status}, %{display} + +=item B<--unknown-link-status> + +Set unknown threshold for status. +Can used special variables like: %{link_status}, %{display} + +=item B<--warning-link-status> + +Set warning threshold for status. +Can used special variables like: %{link_status}, %{display} + +=item B<--critical-link-status> + +Set critical threshold for status (Default: '%{link_status} =~ /failed/i'). +Can used special variables like: %{link_status}, %{display} + +=item B<--warning-*> B<--critical-*> + +Thresholds. +Can be: 'total-online', 'total-offline', 'total-alerting', +'traffic-in', 'traffic-out', 'connections-success', 'connections-auth', +'connections-assoc', 'connections-dhcp', 'connections-dns'. + +=back + +=cut diff --git a/network/versa/director/restapi/plugin.pm b/network/versa/director/restapi/plugin.pm new file mode 100644 index 000000000..e2304558f --- /dev/null +++ b/network/versa/director/restapi/plugin.pm @@ -0,0 +1,49 @@ +# +# Copyright 2020 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 network::versa::director::restapi::plugin; + +use strict; +use warnings; +use base qw(centreon::plugins::script_custom); + +sub new { + my ($class, %options) = @_; + my $self = $class->SUPER::new(package => __PACKAGE__, %options); + bless $self, $class; + + $self->{version} = '1.0'; + $self->{modes} = { + 'devices' => 'network::versa::director::restapi::mode::devices' + }; + + $self->{custom_modes}->{api} = 'network::versa::director::restapi::custom::api'; + return $self; +} + +1; + +__END__ + +=head1 PLUGIN DESCRIPTION + +Check Versa SD-Wan through Director REST API. + +=cut