You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bulk-processor/lib/NGCP/BulkProcessor/RestConnectors/NGCPRestApi.pm

425 lines
11 KiB

package NGCP::BulkProcessor::RestConnectors::NGCPRestApi;
use strict;
## no critic
use threads qw();
use threads::shared qw(shared_clone);
use HTTP::Status qw(:constants :is status_message);
use Encode qw();
use JSON -support_by_pp, -no_export;
use IO::Uncompress::Unzip qw();
use NGCP::BulkProcessor::Globals qw(
$LongReadLen_limit
$working_path
);
use NGCP::BulkProcessor::Logging qw(
getlogger
restdebug
restinfo
);
use NGCP::BulkProcessor::LogError qw(
resterror
restwarn
restrequesterror
restresponseerror
fileerror
);
use NGCP::BulkProcessor::RestConnector qw(_add_headers convert_bools);
use NGCP::BulkProcessor::Calendar qw(get_fake_now_string);
use NGCP::BulkProcessor::Utils qw(makepath cleanupdir booltostring);
require Exporter;
our @ISA = qw(Exporter NGCP::BulkProcessor::RestConnector);
our @EXPORT_OK = qw(
$ITEM_REL_PARAM
cleanupcertfiles
);
my $defaulturi = 'https://127.0.0.1:443';
my $defaultusername = 'administrator';
my $defaultpassword = 'administrator';
my $defaultrealm = 'api_admin_http';
my $defaultcrtauth = 1;
my $API_CERT_DIR = 'apicerts/';
my $API_CERT_FILENAME_FORMAT = '%s%s.pem';
my $default_collection_page_size = 10;
my $first_collection_page_num = 1;
my $first_page_num = 1;
my $contenttype = 'application/json';
my $patchcontenttype = 'application/json-patch+json';
my $defaultfaketime = 0;
my $faketime_header = 'X-Fake-Clienttime';
our $ITEM_REL_PARAM = 'item_rel';
#my $logger = getlogger(__PACKAGE__);
my $request_charset = 'utf-8';
my $response_charset = 'utf-8';
my $timeout = 5*60;
sub _get_api_cert_dir {
return $working_path . $API_CERT_DIR;
}
sub cleanupcertfiles {
return cleanupdir(_get_api_cert_dir(),1,\&filewarn,getlogger(__PACKAGE__));
}
sub new {
my $class = shift;
my $self = NGCP::BulkProcessor::RestConnector->new(@_);
bless($self,$class);
my $lock :shared;
$self->{lock_ref} = \$lock;
$self->setup();
restdebug($self,__PACKAGE__ . ' connector created',getlogger(__PACKAGE__));
return $self;
}
sub setup {
my $self = shift;
my ($baseuri,$username,$password,$realm,$crtauth,$faketime) = @_;
$self->baseuri($baseuri // $defaulturi);
$self->{username} = $username // $defaultusername;
$self->{password} = $password // $defaultpassword;
$self->{realm} = $realm // $defaultrealm;
$self->{faketime} = $faketime // $defaultfaketime;
$self->{crtauth} = $crtauth // $defaultcrtauth;
$self->{crt_path} = undef;
}
sub connectidentifier {
my $self = shift;
if ($self->{uri}) {
return ($self->{username} ? $self->{username} . '@' : '') . $self->{uri};
} else {
return undef;
}
}
sub _setup_ua {
my $self = shift;
my ($ua,$netloc) = @_;
$ua->ssl_opts(
verify_hostname => 0,
SSL_verify_mode => 0,
);
$ua->timeout($timeout) if $timeout;
if ($self->{username}) {
$ua->credentials($netloc, $self->{realm}, $self->{username}, $self->{password});
}
$self->_init_ssl_cert($ua) if $self->{crtauth};
restdebug($self,"ua configured",getlogger(__PACKAGE__));
}
sub _init_ssl_cert {
my ($self, $ua) = @_;
lock ${$self->{lock_ref}};
if (not defined $self->{crt_path}) { #($crtauth // $defaultcrtauth) && $self->{username}) {
makepath(_get_api_cert_dir(),\&fileerror,getlogger(__PACKAGE__));
$self->{crt_path} = sprintf($API_CERT_FILENAME_FORMAT,_get_api_cert_dir(),$self->{username}=~s/[^a-z0-9 _]/_/gir);
}
unless(-f $self->{crt_path}) {
my $res = $ua->post(
$self->_get_request_uri('/api/admincerts/'),
Content_Type => $contenttype,
Content => '{}'
);
if ($res->is_success) {
my $zip = $res->decoded_content;
my $z = IO::Uncompress::Unzip->new(\$zip, MultiStream => 0, Append => 1);
my $data;
while(!$z->eof() && (my $hdr = $z->getHeaderInfo())) {
unless($hdr->{Name} =~ /\.pem$/) {
# wrong file, just read stream, clear buffer and try next
while($z->read($data) > 0) {}
$data = undef;
$z->nextStream();
next;
}
while($z->read($data) > 0) {}
last;
}
$z->close();
if ($data) {
if (open(my $fh,">:raw",$self->{crt_path})) {
print $fh $data;
close $fh;
restdebug($self,"$self->{crt_path} saved",getlogger(__PACKAGE__));
} else {
fileerror("failed to open $self->{crt_path}: $!",getlogger(__PACKAGE__));
}
} else {
resterror($self,'failed to find PEM file in client certificate zip file',getlogger(__PACKAGE__));
}
} else {
resterror($self,'failed to fetch client certificate: '. $res->code . ' ' . $res->message,getlogger(__PACKAGE__));
}
} else {
restdebug($self,"$self->{crt_path} found",getlogger(__PACKAGE__));
}
$ua->ssl_opts(
SSL_cert_file => $self->{crt_path},
SSL_key_file => $self->{crt_path},
);
}
sub _encode_request_content {
my $self = shift;
my ($data) = @_;
return Encode::encode($request_charset,JSON::to_json($data,{ allow_nonref => 1, allow_blessed => 1, convert_blessed => 1, pretty => 0, }));
}
sub _decode_response_content {
my $self = shift;
my ($data) = @_;
my $decoded;
$decoded = JSON::from_json(Encode::decode($response_charset,$data),{ allow_nonref => 1, }) if $data;
convert_bools($decoded);
return $decoded // $data;
}
sub _add_post_headers {
my $self = shift;
my ($req,$headers) = @_;
_add_headers($req,{
'Content-Type' => $contenttype,
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
});
# allow providing custom headers to post(),
# e.g { 'X-Fake-Clienttime' => ... }
$self->SUPER::_add_post_headers($req,$headers);
}
sub _add_get_headers {
my $self = shift;
my ($req,$headers) = @_;
_add_headers($req,{
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
});
$self->SUPER::_add_get_headers($req,$headers);
}
sub _add_patch_headers {
my $self = shift;
my ($req,$headers) = @_;
_add_headers($req,{
'Prefer' => 'return=representation',
'Content-Type' => $patchcontenttype,
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
});
$self->SUPER::_add_patch_headers($req,$headers);
}
sub _encode_patch_content {
my $self = shift;
my ($data) = @_;
return JSON::to_json(
[ map { local $_ = $_; { op => 'replace', path => '/'.$_ , value => $data->{$_} }; } keys %$data ]
);
}
sub _add_put_headers {
my $self = shift;
my ($req,$headers) = @_;
_add_headers($req,{
'Prefer' => 'return=representation',
'Content-Type' => $contenttype,
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
});
$self->SUPER::_add_put_headers($req,$headers);
}
sub _add_delete_headers {
my $self = shift;
my ($req,$headers) = @_;
_add_headers($req,{
($self->{faketime} ? ($faketime_header => get_fake_now_string()) : ()),
});
$self->SUPER::_add_delete_headers($req,$headers);
}
sub _get_page_num_query_param {
my $self = shift;
my ($page_num) = @_;
if (defined $page_num and length($page_num) > 0) {
return 'page=' . $page_num;
}
return undef;
#if (defined $page_num) {
# $page_num += $first_page_num;
#} else {
# $page_num = $first_page_num;
#}
#return (defined $page_num ? 'p=' . $page_num : undef);
}
sub _get_page_size_query_param {
my $self = shift;
my ($page_size) = @_;
$page_size //= $default_collection_page_size;
return 'size=' . $page_size;
}
sub _get_total_count_expected_query_param {
my $self = shift;
my ($total_count_expected) = @_;
return ($total_count_expected ? '' : 'no_count=' . booltostring(1));
}
sub extract_collection_items {
my $self = shift;
my ($data,$page_size,$page_num,$params) = @_;
my $result = undef;
if (defined $data and 'HASH' eq ref $data
and defined $data->{'_embedded'} and 'HASH' eq ref $data->{'_embedded'}) {
$result = $data->{'_embedded'}->{$params->{$ITEM_REL_PARAM}};
if ('ARRAY' eq ref $result) {
} elsif ('HASH' eq ref $result) {
$result = [ $result ];
} else {
undef $result;
}
}
$result //= [];
return shared_clone($result);
}
sub get_defaultcollectionpagesize {
my $self = shift;
return $default_collection_page_size;
}
sub get_firstcollectionpagenum {
my $self = shift;
return $first_collection_page_num;
}
sub _request_error {
my $self = shift;
my $msg = undef;
if (defined $self->responsedata()
and 'HASH' eq ref $self->responsedata()) {
$msg = $self->responsedata()->{'message'};
}
resterror($self,$self->response->code . ' ' . $self->response->message .
(defined $msg && length($msg) > 0 ? ': ' . $msg : ''),getlogger(__PACKAGE__));
}
sub _extract_ids_from_response_location {
my $self = shift;
my $location = $self->response()->header('Location');
my @ids = ();
foreach my $segment (split('/',$location)) {
push(@ids,$segment) if $segment =~ /^\d+$/;
}
return @ids;
}
sub get {
my $self = shift;
if ($self->_get(@_)->code() != HTTP_OK) {
$self->_request_error();
return undef;
} else {
return $self->responsedata();
}
}
sub post {
my $self = shift;
if ($self->_post(@_)->code() != HTTP_CREATED) {
$self->_request_error();
return ();
} else {
return $self->_extract_ids_from_response_location();
}
}
sub post_get {
my $self = shift;
my ($path_query,$post_headers,$get_headers) = @_;
if ($self->_post($path_query,$post_headers)->code() != HTTP_CREATED) {
$self->_request_error();
return undef;
} else {
my @ids = $self->_extract_ids_from_response_location();
my $item = $self->get($self->response()->header('Location'),$get_headers);
$item->{id} = $ids[0] if (scalar @ids) > 0;
return $item;
}
}
sub put {
my $self = shift;
if ($self->_put(@_)->code() != HTTP_OK) {
$self->_request_error();
return undef;
} else {
return $self->responsedata();
}
}
sub patch {
my $self = shift;
if ($self->_patch(@_)->code() != HTTP_OK) {
$self->_request_error();
return undef;
} else {
return $self->responsedata();
}
}
sub delete {
my $self = shift;
if ($self->_delete(@_)->code() != HTTP_NO_CONTENT) {
$self->_request_error();
return 0;
} else {
return 1;
}
}
sub faketime {
my $self = shift;
if (@_) {
$self->{faketime} = shift;
restdebug($self,"fake time " . ($self->{faketime} ? 'enabled' : 'disabled'),getlogger(__PACKAGE__));
}
return $self->{faketime};
}
1;