Skip to content

Commit

Permalink
enh(custommode): add support of 3CX v18 and higher (#5093)
Browse files Browse the repository at this point in the history
  • Loading branch information
omercier authored Aug 7, 2024
1 parent d635b1c commit 0e25233
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 38 deletions.
177 changes: 140 additions & 37 deletions src/apps/voip/3cx/restapi/custom/api.pm
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,13 @@ sub new {
$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' },
'3cx-version:s' => { name => 'version_3cx' },
'api-username:s' => { name => 'api_username' },
'api-password:s' => { name => 'api_password' },
'timeout:s' => { name => 'timeout', default => 30 },
Expand All @@ -56,7 +57,7 @@ sub new {
'critical-http-status:s' => { name => 'critical_http_status' }
});
}

$options{options}->add_help(package => __PACKAGE__, sections => 'REST API OPTIONS', once => 1);

$self->{output} = $options{output};
Expand All @@ -77,16 +78,17 @@ sub set_defaults {}
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} : 443;
$self->{proto} = (defined($self->{option_results}->{proto})) ? $self->{option_results}->{proto} : 'https';
$self->{timeout} = (defined($self->{option_results}->{timeout})) ? $self->{option_results}->{timeout} : 30;
$self->{ssl_opt} = (defined($self->{option_results}->{ssl_opt})) ? $self->{option_results}->{ssl_opt} : undef;
$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->{unknown_http_status} = (defined($self->{option_results}->{unknown_http_status})) ? $self->{option_results}->{unknown_http_status} : '%{http_code} < 200 or %{http_code} >= 300' ;
$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} : '';
$self->{hostname} = (defined($self->{option_results}->{hostname})) ? $self->{option_results}->{hostname} : '';
$self->{port} = (defined($self->{option_results}->{port})) ? $self->{option_results}->{port} : 443;
$self->{proto} = (defined($self->{option_results}->{proto})) ? $self->{option_results}->{proto} : 'https';
$self->{version_3cx} = (defined($self->{option_results}->{version_3cx})) ? $self->{option_results}->{version_3cx} : '';
$self->{timeout} = (defined($self->{option_results}->{timeout})) ? $self->{option_results}->{timeout} : 30;
$self->{ssl_opt} = (defined($self->{option_results}->{ssl_opt})) ? $self->{option_results}->{ssl_opt} : undef;
$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->{unknown_http_status} = (defined($self->{option_results}->{unknown_http_status})) ? $self->{option_results}->{unknown_http_status} : '%{http_code} < 200 or %{http_code} >= 300' ;
$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.');
Expand All @@ -100,7 +102,7 @@ sub check_options {
$self->{output}->add_option_msg(short_msg => 'Need to specify --api-password option.');
$self->{output}->option_exit();
}

$self->{option_results}->{api_version} = $self->get_api_version(version_3cx => $self->{option_results}->{version_3cx});
$self->{cache}->check_options(option_results => $self->{option_results});

return 0;
Expand All @@ -123,8 +125,12 @@ sub settings {
$self->{http}->add_header(key => 'Content-Type', value => 'application/json;charset=UTF-8');
if (defined($self->{cookie})) {
$self->{http}->add_header(key => 'Cookie', value => $self->{cookie});
if (defined($self->{xsrf})) {
$self->{http}->add_header(key => 'X-XSRF-TOKEN', value => $self->{xsrf});

if (defined($self->{auth_header})) {
my $auth_header_key = ( $self->{option_results}->{api_version} == 1 )
? 'X-XSRF-TOKEN'
: 'Authorization';
$self->{http}->add_header(key => $auth_header_key, value => $self->{auth_header});
}
}
$self->{http}->set_options(%{$self->{option_results}});
Expand All @@ -135,10 +141,10 @@ sub authenticate {

my $has_cache_file = $options{statefile}->read(statefile => '3cx_api_' . md5_hex($self->{option_results}->{hostname}) . '_' . md5_hex($self->{option_results}->{api_username}));
my $cookie = $options{statefile}->get(name => 'cookie');
my $xsrf = $options{statefile}->get(name => 'xsrf');
my $auth_header = $options{statefile}->get(name => 'auth_header');
my $expires_on = $options{statefile}->get(name => 'expires_on');

if ($has_cache_file == 0 || !defined($cookie) || !defined($xsrf) || (($expires_on - time()) < 10)) {
if ($has_cache_file == 0 || !defined($cookie) || !defined($auth_header) || (($expires_on - time()) < 10)) {
my $post_data = '{"Username":"' . $self->{api_username} . '",' .
'"Password":"' . $self->{api_password} . '"}';

Expand All @@ -161,17 +167,49 @@ sub authenticate {
$self->{output}->add_option_msg(short_msg => "Error retrieving cookie");
$self->{output}->option_exit();
}
# 3CX 16.0.5.611 does not use XSRF-TOKEN anymore
if (defined ($header) && $header =~ /(?:^| )XSRF-TOKEN=([^;]+);.*/) {
$xsrf = $1;

my $data;
if ($self->{option_results}->{api_version} == 1)
{
# for 3CX versions prior to 18.0.5
# 3CX 16.0.5.611 does not use XSRF-TOKEN anymore
if (defined ($header) && $header =~ /(?:^| )XSRF-TOKEN=([^;]+);.*/) {
$auth_header = $1;
}
$data = { last_timestamp => time(), cookie => $cookie, xsrf => $auth_header, expires_on => time() + (3600 * 24) };
} else {
# for 3CX versions higher or equal to 18.0.5
$self->{http}->add_header(key => 'Cookie', value => $cookie);
$content = $self->{http}->request(
method => 'GET',
url_path => '/api/Token',
unknown_status => $self->{unknown_http_status},
warning_status => $self->{warning_http_status},
critical_status => $self->{critical_http_status}
);
my $decoded;
eval {
$decoded = JSON::XS->new->decode($content);
};
if ($@) {
$self->{output}->add_option_msg(short_msg => "Cannot decode json response: $@");
$self->{output}->option_exit();
}
if (!defined($decoded)) {
$self->{output}->add_option_msg(short_msg => "Error while retrieving data (add --debug option for detailed message)");
$self->{output}->option_exit();
}
$auth_header = $decoded->{token_type} . " " . $decoded->{access_token};
$expires_on = time() + ($decoded->{expires_in} * 60);

$data = { last_timestamp => time(), cookie => $cookie, bearer => $auth_header, expires_on => $expires_on };
}

my $datas = { last_timestamp => time(), cookie => $cookie, xsrf => $xsrf, expires_on => time() + (3600 * 24) };
$options{statefile}->write(data => $datas);
$options{statefile}->write(data => $data);
}

$self->{cookie} = $cookie;
$self->{xsrf} = $xsrf;
$self->{auth_header} = $auth_header;
}

sub request_api {
Expand Down Expand Up @@ -271,7 +309,7 @@ sub api_system_status {
return $status;
}

sub internal_update_checker {
sub internal_update_checker_v1 {
my ($self, %options) = @_;

my $status = $self->request_api(method => 'GET', url_path =>'/api/UpdateChecker/GetFromParams', eval_content => 1);
Expand All @@ -285,58 +323,123 @@ sub internal_update_checker {
return $status;
}

sub api_update_checker {
sub internal_update_checker_v2 {
my ($self, %options) = @_;

my $status = $self->internal_update_checker();
my $status = $self->request_api(method => 'GET', url_path =>'/xapi/v1/GetUpdatesStats()');
if (ref($status) eq 'HASH') {
$status = $status->{TcxUpdate};
if (ref($status) ne 'ARRAY') {
# See above note about strange content
$status = JSON::XS->new->decode($status);
}
}
return $status;
}


sub api_update_checker {
my ($self, %options) = @_;

if ($self->{option_results}->{api_version} == 1){
return $self->internal_update_checker_v1();
}
return $self->internal_update_checker_v2();
}

sub get_api_version {
my ($self, %options) = @_;

# Given the provided (or not) 3cx version, determine once and for all the API version
# This API version is an internal reference in centreon-plugins
# Version 1 corresponds to versions prior to v18 update 5 (<= 18.0.4.x)
# Version 2 corresponds to versions greater or equal to v18 update 5 (> 18.0.5.0)

# assuming the lastest API version if not provided
return 2 if ( !defined($options{version_3cx}) );

my @version_decomposition = $options{version_3cx} =~ /^([0-9]+)\.?([0-9]*)\.?([0-9]*)\.?([0-9]*)$/;

if (scalar(@version_decomposition) == 0){
$self->{output}->add_option_msg(
debug => 1,
long_msg => "Version '" . $options{version_3cx} . "' not formatted properly. Switching to latest supported version.");
return 2;
}

if ($version_decomposition[0] < 18
or $version_decomposition[0] == 18
and defined($version_decomposition[1]) and $version_decomposition[1] == 0
and defined($version_decomposition[2]) and $version_decomposition[2] < 5) {

$self->{output}->add_option_msg(
debug => 1,
long_msg => "Version '" . $options{version_3cx} . "' identified as prior to 18 update 5. Using old API.");
return 1;
} else {
$self->{output}->add_option_msg(
debug => 1,
long_msg => "Version '" . $options{version_3cx} . "' identified as higher or equal to 18 update 5. Using new API.");
return 2;
}
}

1;

__END__
=head1 NAME
3CX Rest API
3CX Rest API module
=head1 REST API OPTIONS
=over 8
=item B<--hostname>
Set hostname or IP of 3CX server.
Define the name or the address of the 3CX server.
=item B<--port>
Set 3CX Port (default: '443').
Define the port to connect to (default: '443').
=item B<--proto>
Specify http if needed (default: 'https').
Define the protocol to reach the API (default: 'https').
=item B<--3cx-version>
Define the version of 3CX to monitor for the plugin to adapt to the API version. If this option is omitted, the plugin will assume the API is in the latest supported version.
Example: 18.0.9.20 for version 18 update 9.
=item B<--api-username>
Set 3CX Username.
Define the username for authentication.
=item B<--api-password>
Set 3CX Password.
Define the password associated with the username.
=item B<--timeout>
Threshold for HTTP timeout (default: '30').
Define the timeout in seconds (default: 30).
=item B<--unknown-http-status>
Threshold unknown for http response code.
(default: '%{http_code} < 200 or %{http_code} >= 300')
Define the conditions to match on the HTTP Status for the returned status to be UNKNOWN.
Default: '%{http_code} < 200 or %{http_code} >= 300'
=item B<--warning-http-status>
Warning threshold for http response code.
Define the conditions to match on the HTTP Status for the returned status to be WARNING.
Example: '%{http_code} == 500'
=item B<--critical-http-status>
Critical threshold for http response code.
Define the conditions to match on the HTTP Status for the returned status to be CRITICAL.
Example: '%{http_code} == 500'
=back
Expand Down
2 changes: 1 addition & 1 deletion src/apps/voip/3cx/restapi/plugin.pm
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ __END__
=head1 PLUGIN DESCRIPTION
Check 3CX ressources through its HTTPS remote API.
Monitor 3CX resources through its HTTPS API.
Requirements: at least 3CX 15.5.
Expand Down
2 changes: 2 additions & 0 deletions tests/resources/spellcheck/stopwords.t
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--3cx-version
--add-sysdesc
--api-filter-orgs
--api-password
Expand Down Expand Up @@ -49,6 +50,7 @@
-InputFormat
-NoLogo
2c
3CX
ADSL
ASAM
Alcatel
Expand Down

0 comments on commit 0e25233

Please sign in to comment.