From e9580d2e9d2b4e96de11ea502d07200356d6c04d Mon Sep 17 00:00:00 2001 From: Gerhard Jungwirth Date: Thu, 17 Dec 2015 11:02:01 +0100 Subject: [PATCH] MT#15883 implement rtcnetworks API GET,item PUT, item PATCH implemented this covers CRUD of rtcnetworks Change-Id: If64034161886b5c7f943fd5e9a4d651e131c5a52 --- lib/NGCP/Panel/Controller/API/Resellers.pm | 11 +- lib/NGCP/Panel/Controller/API/RtcNetworks.pm | 135 +++++++++++ .../Panel/Controller/API/RtcNetworksItem.pm | 228 ++++++++++++++++++ lib/NGCP/Panel/Form/Rtc/NetworksAdmin.pm | 22 ++ lib/NGCP/Panel/Form/Rtc/NetworksReseller.pm | 89 +++++++ lib/NGCP/Panel/Role/API/Resellers.pm | 12 +- lib/NGCP/Panel/Role/API/RtcNetworks.pm | 138 +++++++++++ lib/NGCP/Panel/Utils/ComxAPIClient.pm | 10 +- lib/NGCP/Panel/Utils/Rtc.pm | 153 +++++++++++- t/api-rest/api-rtc-resellers.t | 91 +++++++ 10 files changed, 870 insertions(+), 19 deletions(-) create mode 100644 lib/NGCP/Panel/Controller/API/RtcNetworks.pm create mode 100644 lib/NGCP/Panel/Controller/API/RtcNetworksItem.pm create mode 100644 lib/NGCP/Panel/Form/Rtc/NetworksAdmin.pm create mode 100644 lib/NGCP/Panel/Form/Rtc/NetworksReseller.pm create mode 100644 lib/NGCP/Panel/Role/API/RtcNetworks.pm create mode 100644 t/api-rest/api-rtc-resellers.t diff --git a/lib/NGCP/Panel/Controller/API/Resellers.pm b/lib/NGCP/Panel/Controller/API/Resellers.pm index a6920b7f2e..a754fc62af 100644 --- a/lib/NGCP/Panel/Controller/API/Resellers.pm +++ b/lib/NGCP/Panel/Controller/API/Resellers.pm @@ -190,10 +190,13 @@ sub POST :Allow { contract_id => $resource->{contract_id}, }); NGCP::Panel::Utils::Reseller::create_email_templates( c => $c, reseller => $reseller ); - NGCP::Panel::Utils::Rtc::modify_reseller_rtc(undef, $resource, $c->config, - $reseller, sub { - $c->log->warn(shift); return; - }); + NGCP::Panel::Utils::Rtc::modify_reseller_rtc( + resource => $resource, + config => $c->config, + reseller_item => $reseller, + err_code => sub { + $c->log->warn(shift); return; + }); } catch($e) { $c->log->error("failed to create reseller: $e"); # TODO: user, message, trace, ... $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create reseller."); diff --git a/lib/NGCP/Panel/Controller/API/RtcNetworks.pm b/lib/NGCP/Panel/Controller/API/RtcNetworks.pm new file mode 100644 index 0000000000..c2e86e522c --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/RtcNetworks.pm @@ -0,0 +1,135 @@ +package NGCP::Panel::Controller::API::RtcNetworks; +use NGCP::Panel::Utils::Generic qw(:all); +use Moose; +use boolean qw(true); +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); + +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +class_has 'api_description' => ( + is => 'ro', + isa => 'Str', + default => + 'Show a collection of RTC networks, belonging to a specific reseller.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + ]}, +); + +with 'NGCP::Panel::Role::API::RtcNetworks'; + +class_has('resource_name', is => 'ro', default => 'rtcnetworks'); +class_has('dispatch_path', is => 'ro', default => '/api/rtcnetworks/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-rtcnetworks'); + +__PACKAGE__->config( + action => { + map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 0, + Does => [qw(ACL CheckTrailingSlash RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }, + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); + return 1; +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $resellers = $self->item_rs($c); + (my $total_count, $resellers) = $self->paginate_order_collection($c, $resellers); + my (@embedded, @links); + for my $subscriber ($resellers->all) { + push @embedded, $self->hal_from_item($c, $subscriber); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('%s%d', $self->dispatch_path, $subscriber->id), + ); + } + push @links, + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf('%s?page=%s&rows=%s', $self->dispatch_path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('%s?page=%d&rows=%d', $self->dispatch_path, $page + 1, $rows)); + } + if($page > 1) { + push @links, Data::HAL::Link->new(relation => 'prev', href => sprintf('/%s?page=%d&rows=%d', $c->request->path, $page - 1, $rows)); + } + + my $hal = Data::HAL->new( + embedded => [@embedded], + links => [@links], + ); + $hal->resource({ + total_count => $total_count, + }); + my $response = HTTP::Response->new(HTTP_OK, undef, + HTTP::Headers->new($hal->http_headers(skip_links => 1)), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/RtcNetworksItem.pm b/lib/NGCP/Panel/Controller/API/RtcNetworksItem.pm new file mode 100644 index 0000000000..f199a17219 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/RtcNetworksItem.pm @@ -0,0 +1,228 @@ +package NGCP::Panel::Controller::API::RtcNetworksItem; +use NGCP::Panel::Utils::Generic qw(:all); +use Moose; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); + +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::RtcNetworks'; + +class_has('resource_name', is => 'ro', default => 'rtcnetworks'); +class_has('dispatch_path', is => 'ro', default => '/api/rtcnetworks/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-rtcnetworks'); + +class_has(@{ __PACKAGE__->get_journal_query_params() }); + +__PACKAGE__->config( + action => { + (map { $_ => { + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Args => 1, + Does => [qw(ACL RequireSSL)], + Method => $_, + Path => __PACKAGE__->dispatch_path, + } } @{ __PACKAGE__->allowed_methods }), + @{ __PACKAGE__->get_journal_action_config(__PACKAGE__->resource_name,{ + ACLDetachTo => '/api/root/invalid_user', + AllowedRole => [qw/admin reseller/], + Does => [qw(ACL RequireSSL)], + }) }, + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); + return 1; +} + +sub GET :Allow { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, reseller => $item); + + my $hal = $self->hal_from_item($c, $item); + + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + (map { # XXX Data::HAL must be able to generate links with multiple relations + s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r + =~ s/rel=self/rel="item self"/r; + } $hal->http_headers), + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->body($response->content); + return; + } + return; +} + +sub HEAD :Allow { + my ($self, $c, $id) = @_; + $c->forward(qw(GET)); + $c->response->body(q()); + return; +} + +sub OPTIONS :Allow { + my ($self, $c, $id) = @_; + my $allowed_methods = $self->allowed_methods_filtered($c); + $c->response->headers(HTTP::Headers->new( + Allow => join(', ', @{ $allowed_methods }), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub PUT :Allow { + my ($self, $c, $id) = @_; + my $schema = $c->model('DB'); + my $guard = $schema->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $reseller = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, reseller => $reseller); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + + my $form = $self->get_form($c); + my $old_resource = $self->hal_from_item($c, $reseller, 1)->resource; + $reseller = $self->update_item($c, $reseller, $old_resource, $resource, $form); + last unless $reseller; + + my $hal = $self->hal_from_item($c, $reseller); + last unless $self->add_update_journal_item_hal($c,{ hal => $hal, id => $reseller->id }); + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + #my $hal = $self->hal_from_item($c, $reseller); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub PATCH :Allow { + my ($self, $c, $id) = @_; + my $schema = $c->model('DB'); + my $guard = $schema->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $reseller = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, reseller => $reseller); + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ops => ["add", "replace", "copy", "remove"], + ); + last unless $json; + + my $form = $self->get_form($c); + my $old_resource = $self->hal_from_item($c, $reseller, 1)->resource; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + $reseller = $self->update_item($c, $reseller, $old_resource, $resource, $form); + last unless $reseller; + + my $hal = $self->hal_from_item($c, $reseller); + last unless $self->add_update_journal_item_hal($c,{ hal => $hal, id => $reseller->id }); + + $guard->commit; + + if ('minimal' eq $preference) { + $c->response->status(HTTP_NO_CONTENT); + $c->response->header(Preference_Applied => 'return=minimal'); + $c->response->body(q()); + } else { + #my $hal = $self->hal_from_item($c, $reseller); + my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( + $hal->http_headers, + ), $hal->as_json); + $c->response->headers($response->headers); + $c->response->header(Preference_Applied => 'return=representation'); + $c->response->body($response->content); + } + } + return; +} + +sub item_base_journal :Journal { + my $self = shift @_; + return $self->handle_item_base_journal(@_); +} + +sub journals_get :Journal { + my $self = shift @_; + return $self->handle_journals_get(@_); +} + +sub journalsitem_get :Journal { + my $self = shift @_; + return $self->handle_journalsitem_get(@_); +} + +sub journals_options :Journal { + my $self = shift @_; + return $self->handle_journals_options(@_); +} + +sub journalsitem_options :Journal { + my $self = shift @_; + return $self->handle_journalsitem_options(@_); +} + +sub journals_head :Journal { + my $self = shift @_; + return $self->handle_journals_head(@_); +} + +sub journalsitem_head :Journal { + my $self = shift @_; + return $self->handle_journalsitem_head(@_); +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return 1; +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Rtc/NetworksAdmin.pm b/lib/NGCP/Panel/Form/Rtc/NetworksAdmin.pm new file mode 100644 index 0000000000..6694598287 --- /dev/null +++ b/lib/NGCP/Panel/Form/Rtc/NetworksAdmin.pm @@ -0,0 +1,22 @@ +package NGCP::Panel::Form::Rtc::NetworksAdmin; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::Rtc::NetworksReseller'; + +has_field 'reseller' => ( + type => '+NGCP::Panel::Field::Reseller', + #validate_when_empty => 1, + element_attr => { + rel => ['tooltip'], + title => ['The reseller id this networks belong to.'], + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/reseller rtc_user_id networks/], +); + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Rtc/NetworksReseller.pm b/lib/NGCP/Panel/Form/Rtc/NetworksReseller.pm new file mode 100644 index 0000000000..a5bbb6d197 --- /dev/null +++ b/lib/NGCP/Panel/Form/Rtc/NetworksReseller.pm @@ -0,0 +1,89 @@ +package NGCP::Panel::Form::Rtc::NetworksReseller; +use HTML::FormHandler::Moose; +use HTML::FormHandler::Widget::Block::Bootstrap; +extends 'HTML::FormHandler'; + +# with 'NGCP::Panel::Render::RepeatableJs'; # only used in API currently + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +has_field 'submitid' => ( type => 'Hidden' ); +sub build_render_list {return [qw/submitid fields actions/]} +sub build_form_element_class {return [qw(form-horizontal)]} + +has_field 'rtc_user_id' => ( + # readonly + type => 'Text', + required => 0, + element_attr => { + rel => ['tooltip'], + title => ['ID in the backend RTC API (readonly).'], + }, +); + +has_field 'networks' => ( + type => 'Repeatable', + required => 0, #1, + setup_for_js => 1, + do_wrapper => 1, + do_label => 0, + tags => { + controls_div => 1, + }, + wrapper_class => [qw/hfh-rep/], + element_attr => { + rel => ['tooltip'], + title => ['An array of objects with keys "config", "connector" and "tag" to create RTC networks for this reseller'], + }, +); +# webrtc, conference, xmpp-connector, sip-connector, sipwise-connector +has_field 'networks.connector' => ( + type => 'Select', + options => [ + { value => 'webrtc', label => 'webrtc' }, + { value => 'conference', label => 'conference' }, + { value => 'xmpp-connector', label => 'xmpp-connector' }, + { value => 'sip-connector', label => 'sip-connector' }, + { value => 'sipwise-connector', label => 'sipwise-connector' }, + ], + element_attr => { + rel => ['tooltip'], + title => ['One of the available options. This defines, to which networks rtc subscribers will be able to connect to.'], + }, +); + +has_field 'networks.tag' => ( + type => 'Text', + element_attr => { + rel => ['tooltip'], + title => ['An arbitrary name, to address that network instance'], + }, +); + +has_field 'networks.config' => ( + type => 'Compound', # Text + element_attr => { + rel => ['tooltip'], + title => ['An arbitrary hash of additional config contents; e.g. {"xms": false}'], + }, +); + +has_field 'save' => ( + type => 'Submit', + value => 'Save', + element_class => [qw/btn btn-primary/], + label => '', +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/rtc_user_id networks/], +); + +has_block 'actions' => ( + tag => 'div', + class => [qw/modal-footer/], + render_list => [qw/save/], +); + +1; \ No newline at end of file diff --git a/lib/NGCP/Panel/Role/API/Resellers.pm b/lib/NGCP/Panel/Role/API/Resellers.pm index 241713dc27..debc8687d8 100644 --- a/lib/NGCP/Panel/Role/API/Resellers.pm +++ b/lib/NGCP/Panel/Role/API/Resellers.pm @@ -130,10 +130,14 @@ sub update_reseller { contract_id => $resource->{contract_id}, }); - NGCP::Panel::Utils::Rtc::modify_reseller_rtc($old_resource, $resource, $c->config, - $reseller, sub { - $c->log->warn(shift); return; - }); + NGCP::Panel::Utils::Rtc::modify_reseller_rtc( + old_resource => $old_resource, + resource => $resource, + config => $c->config, + reseller_item => $reseller, + err_code => sub { + $c->log->warn(shift); return; + }); # TODO: should we lock reseller admin logins if reseller gets terminated? # or terminate all his customers and delete non-billing data? diff --git a/lib/NGCP/Panel/Role/API/RtcNetworks.pm b/lib/NGCP/Panel/Role/API/RtcNetworks.pm new file mode 100644 index 0000000000..5e75bc4486 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/RtcNetworks.pm @@ -0,0 +1,138 @@ +package NGCP::Panel::Role::API::RtcNetworks; +use NGCP::Panel::Utils::Generic qw(:all); +use Moose::Role; +with 'NGCP::Panel::Role::API' => { + -alias =>{ item_rs => '_item_rs', }, + -excludes => [ 'item_rs' ], +}; + +use boolean qw(true); +use TryCatch; +use Data::HAL qw(); +use Data::HAL::Link qw(); +use HTTP::Status qw(:constants); +use JSON::Types; + +use NGCP::Panel::Form::Rtc::NetworksAdmin; +use NGCP::Panel::Utils::Subscriber; +use NGCP::Panel::Utils::Rtc; + +sub get_form { + my ($self, $c) = @_; + + return NGCP::Panel::Form::Rtc::NetworksAdmin->new; +} + +sub hal_from_item { + my ($self, $c, $item, $include_id) = @_; + + my $resource = { reseller_id => $item->id}; + if ($item->rtc_user) { + my $rtc_user_id = $item->rtc_user->rtc_user_id; + $resource->{rtc_user_id} = $rtc_user_id if $include_id; + $resource->{networks} = NGCP::Panel::Utils::Rtc::get_rtc_networks( + rtc_user_id => $rtc_user_id, + config => $c->config, + include_id => $include_id, + err_code => sub { + $c->log->warn(shift); return; + }); + } else { + } + + my $hal = Data::HAL->new( + links => [ + Data::HAL::Link->new( + relation => 'curies', + href => 'http://purl.org/sipwise/ngcp-api/#rel-{rel}', + name => 'ngcp', + templated => true, + ), + Data::HAL::Link->new(relation => 'collection', href => sprintf("/api/%s/", $self->resource_name)), + Data::HAL::Link->new(relation => 'profile', href => 'http://purl.org/sipwise/ngcp-api/'), + Data::HAL::Link->new(relation => 'self', href => sprintf("%s%d", $self->dispatch_path, $item->id)), + Data::HAL::Link->new(relation => 'ngcp:resellers', href => sprintf("/api/resellers/%d", $item->id)), + $self->get_journal_relation_link($item->id), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + my $form = $self->get_form($c); + unless ($include_id) { + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + run => 0, + exceptions => ['rtc_user_id'], + ); + } + + $hal->resource($resource); + return $hal; +} + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs; + $item_rs = $c->model('DB')->resultset('resellers') + ->search_rs(undef, { + prefetch => 'rtc_user', + }); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ + id => $c->user->reseller_id, + }); + } + + return $item_rs; +} + +sub item_by_id { + my ($self, $c, $id) = @_; + + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + my $reseller = $item; + + if (ref $resource->{networks} ne "ARRAY") { + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'networks'. Must be an array."); + return; + } + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + + NGCP::Panel::Utils::Rtc::modify_rtc_networks( + old_resource => $old_resource, + resource => $resource, + config => $c->config, + reseller_item => $reseller, + err_code => sub { + $c->log->warn(shift); return; + }); + + try { + + } catch($e) { + $c->log->error("failed to update autoattendants: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to update autoattendants."); + return; + }; + + return $reseller; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Utils/ComxAPIClient.pm b/lib/NGCP/Panel/Utils/ComxAPIClient.pm index 854cb91d37..aabd122fb2 100644 --- a/lib/NGCP/Panel/Utils/ComxAPIClient.pm +++ b/lib/NGCP/Panel/Utils/ComxAPIClient.pm @@ -14,7 +14,7 @@ use JSON qw/decode_json encode_json/; has 'ua' => ( is => 'rw', default => sub { return LWP::UserAgent->new( - ssl_opts => { verify_hostname => 0 }, + ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 }, timeout => 20, ); }); @@ -147,7 +147,7 @@ sub get_session { my $ua = $self->ua; return $self->_create_response( - $ua->get($self->host . "/sessions/id/$session_id") + $ua->get($self->host . "/sessions/id/$session_id"), ); } @@ -175,6 +175,12 @@ sub get_networks { return $networks; } +sub get_networks_by_user_id { + my ($self, $user_id) = @_; + my $networks = $self->_resolve_collection_fast( "/users/id/$user_id/networks" ); + return $networks; +} + sub _resolve_collection { my ($self, $bare_url, $max_rows) = @_; my $ua = $self->ua; diff --git a/lib/NGCP/Panel/Utils/Rtc.pm b/lib/NGCP/Panel/Utils/Rtc.pm index d4fa5bfb38..dba856f1ae 100644 --- a/lib/NGCP/Panel/Utils/Rtc.pm +++ b/lib/NGCP/Panel/Utils/Rtc.pm @@ -6,9 +6,12 @@ use strict; use JSON qw//; use NGCP::Panel::Utils::ComxAPIClient; +use NGCP::Panel::Utils::Generic qw/compare/; sub modify_reseller_rtc { - my ($old_resource, $resource, $config, $reseller_item, $err_code) = @_; + my %params = @_; + my ($old_resource, $resource, $config, $reseller_item, $err_code) = + @params{qw/old_resource resource config reseller_item err_code/}; if (!defined $err_code || ref $err_code ne 'CODE') { $err_code = sub { return 0; }; @@ -21,7 +24,11 @@ sub modify_reseller_rtc { return; } - _create_rtc_user($resource, $config, $reseller_item, $err_code); + _create_rtc_user( + resource => $resource, + config => $config, + reseller_item => $reseller_item, + err_code => $err_code); } elsif ((defined $old_resource) && (defined $resource)) { @@ -30,24 +37,36 @@ sub modify_reseller_rtc { $old_resource->{enable_rtc}) { # just terminated $resource->{enable_rtc} = JSON::false; - _delete_rtc_user($config, $reseller_item, $err_code); + _delete_rtc_user( + config => $config, + reseller_item => $reseller_item, + err_code => $err_code); } elsif ($old_resource->{enable_rtc} && !$resource->{enable_rtc}) { # disable rtc - _delete_rtc_user($config, $reseller_item, $err_code); + _delete_rtc_user( + config => $config, + reseller_item => $reseller_item, + err_code => $err_code); } elsif (!$old_resource->{enable_rtc} && $resource->{enable_rtc} && $resource->{status} ne 'terminated') { # enable rtc - _create_rtc_user($resource, $config, $reseller_item, $err_code); + _create_rtc_user( + resource => $resource, + config => $config, + reseller_item => $reseller_item, + err_code => $err_code); } } return; } sub _create_rtc_user { - my ($resource, $config, $reseller_item, $err_code) = @_; + my %params = @_; + my ($resource, $config, $reseller_item, $err_code) = + @params{qw/resource config reseller_item err_code/}; my $rtc_networks = $resource->{rtc_networks} // []; if ('ARRAY' ne (ref $rtc_networks)) { @@ -103,16 +122,18 @@ sub _create_rtc_user { {xms => JSON::false}, $user->{data}{id}, ); - if ($user->{code} != 201) { + if ($n_response->{code} != 201) { return unless &{$err_code}( - 'Creating rtc network failed. Error code: ' . $user->{code}); + 'Creating rtc network failed. Error code: ' . $n_response->{code}); } } return; } sub _delete_rtc_user { - my ($config, $reseller_item, $err_code) = @_; + my %params = @_; + my ($config, $reseller_item, $err_code) = + @params{qw/config reseller_item err_code/}; my $comx = NGCP::Panel::Utils::ComxAPIClient->new( host => $config->{rtc}{schema}.'://'. @@ -146,6 +167,120 @@ sub _delete_rtc_user { return; } +sub get_rtc_networks { + my %params = @_; + my ($rtc_user_id, $config, $reseller_item, $include_id, $err_code) = + @params{qw/rtc_user_id config reseller_item include_id err_code/}; + + if (!defined $err_code || ref $err_code ne 'CODE') { + $err_code = sub { return 0; }; + } + + my $comx = NGCP::Panel::Utils::ComxAPIClient->new( + host => $config->{rtc}{schema}.'://'. + $config->{rtc}{host}.':'.$config->{rtc}{port}. + $config->{rtc}{path}, + ); + $comx->login( + $config->{rtc}{user}, + $config->{rtc}{pass}, + $config->{rtc}{host}.':'.$config->{rtc}{port}); + if ($comx->login_status->{code} != 200) { + return unless &{$err_code}( + 'Rtc Login failed. Check config settings.'); + } + + my $networks_resp = $comx->get_networks_by_user_id($rtc_user_id); + my $networks = $networks_resp->{data}; + unless (defined $networks && 'ARRAY' eq ref $networks && @{ $networks }) { + return unless &{$err_code}( + 'Fetching networks failed. Code: ' . $networks_resp->{code}); + } + + my $res = [map {{ + config =>$_->{config}, + connector => $_->{connector}, + tag => $_->{tag}, + $include_id ? (id => $_->{id}) : (), + }} @{ $networks }]; + + return $res; +} + +sub modify_rtc_networks { + my %params = @_; + my ($old_resource, $resource, $config, $reseller_item, $err_code) = + @params{qw/old_resource resource config reseller_item err_code/}; + + if (!defined $err_code || ref $err_code ne 'CODE') { + $err_code = sub { return 0; }; + } + + if ((!defined $old_resource) || (!defined $resource)) { # can only modify (no create/delete) the whole resource + return unless &{$err_code}( + 'Cannot Modify rtc network. Old or new resource missing.'); + } + + my $comx = NGCP::Panel::Utils::ComxAPIClient->new( + host => $config->{rtc}{schema}.'://'. + $config->{rtc}{host}.':'.$config->{rtc}{port}. + $config->{rtc}{path}, + ); + $comx->login( + $config->{rtc}{user}, + $config->{rtc}{pass}, + $config->{rtc}{host}.':'.$config->{rtc}{port}); + if ($comx->login_status->{code} != 200) { + return unless &{$err_code}( + 'Rtc Login failed. Check config settings.'); + } + + my (@deleted, @new); + for my $nw (@{ $resource->{networks} }) { + my $nw_tag = $nw->{tag}; + my ($old_nw) = grep {$nw_tag eq $_->{tag}} @{ $old_resource->{networks} }; + if (!defined $old_nw) { + push @new, $nw; + } else { + if ($nw->{connector} ne $old_nw->{connector} + || !compare($nw->{config}, $old_nw->{config}) + ) { + push @deleted, $old_nw; + push @new, $nw; + } + } + } + for my $nw (@{ $old_resource->{networks} }) { + my $nw_tag = $nw->{tag}; + + my ($new_nw) = grep {$nw_tag eq $_->{tag}} @{ $resource->{networks} }; + if (!defined $new_nw) { + push @deleted, $nw; + } + } + + for my $nw (@deleted) { + my $n_response = $comx->delete_network($nw->{id}); + if ($n_response->{code} != 200) { + return unless &{$err_code}( + 'Deleting rtc network failed. Error code: ' . $n_response->{code}); + } + } + for my $nw (@new) { + my $n_response = $comx->create_network( + $nw->{tag}, + $nw->{connector}, + $nw->{config} // {}, + $old_resource->{rtc_user_id}, + ); + if ($n_response->{code} != 201) { + return unless &{$err_code}( + 'Creating rtc network failed. Error code: ' . $n_response->{code}); + } + } + return; +} + 1; # vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-rtc-resellers.t b/t/api-rest/api-rtc-resellers.t new file mode 100644 index 0000000000..596b15ffd5 --- /dev/null +++ b/t/api-rest/api-rtc-resellers.t @@ -0,0 +1,91 @@ +use strict; +use warnings; + +use Test::More; +use Test::Collection; +use Test::FakeData; +use Data::Dumper; + + +unless ($ENV{TEST_RTC}) { + plan skip_all => "not testing rtc, enable TEST_RTC=yes to run tests"; + exit 0; +} + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + 'resellers' => { + 'data' => { + name => "apitest reseller name " . time(), + contract_id => sub { return shift->create('contracts', @_); }, + status => 'active', + enable_rtc => 1, # JSON::false + rtc_networks => ['sip', 'xmpp', 'sipwise'], + }, + }, +}); + +my $test_machine = Test::Collection->new( + name => 'resellers', + embedded_resources => [qw/resellers/], +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; +# store some basic reseller data, to run tests with +$test_machine->DATA_ITEM_STORE($fake_data->process('resellers')); +$test_machine->form_data_item( ); + +my $reseller_id; + +# test reseller API +{ + my ($res, $content) = $test_machine->check_item_get('/api/resellers/?page=1&rows=10', "fetch resellers collection"); + my $req; + ($res, $content, $req) = $test_machine->check_item_post(); + is($res->code, 201, 'create test reseller successful'); + #my $reseller_id = $test_machine->get_id_from_created($res); + ($reseller_id) = $res->header('Location') =~ m/(\d+)$/; + + cmp_ok($reseller_id, '>', 0, 'got valid reseller id'); + ($res, $content) = $test_machine->check_item_get("/api/resellers/$reseller_id/", "fetch created reseller"); + is($res->code, 200, 'reseller successfully retrieved'); + ok($content->{enable_rtc}, 'rtc is enabled on created reseller'); +} + +# test rtcnetworks API +{ + my ($res, $content) = $test_machine->check_item_get("/api/rtcnetworks/$reseller_id", "fetch rtcnetwork"); + is($res->code, 200, 'rtcnetwork successfully retrieved'); + isa_ok($content->{networks}, 'ARRAY', 'networks arrayref exists'); + is(scalar(@{ $content->{networks} }), 3, 'should contain the 3 precreated networks'); + is($content->{networks}[0]{connector}, 'sip-connector', 'First network is of "sip-connector"'); + + ($res, $content) = $test_machine->request_patch( + [ + { op => 'remove', path => '/networks/2'}, + { op => 'replace', path => '/networks/1/connector', value => 'webrtc'}, + ], + "/api/rtcnetworks/$reseller_id/", + ); + is($res->code, 200, 'PATCH operation on rtcnetworks item'); + isa_ok($content->{networks}, 'ARRAY', 'networks arrayref exists'); + is(scalar(@{ $content->{networks} }), 2, 'should be left with 2 networks'); + is($content->{networks}[1]{connector}, 'webrtc', 'Changed one network to "webrtc"'); +} + +{ + my ($res, $content, $req) = $test_machine->request_patch( + [ + { op => 'replace', path => '/status', value => 'terminated' }, + ], + "/api/resellers/$reseller_id/", + ); + is($res->code, 200, 'terminate reseller successful'); +} +$test_machine->clear_test_data_all(); + +done_testing; + +1; + +# vim: set tabstop=4 expandtab: