diff --git a/lib/NGCP/Panel/Controller/API/PeeringGroups.pm b/lib/NGCP/Panel/Controller/API/PeeringGroups.pm new file mode 100644 index 0000000000..a7f0ddf26d --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringGroups.pm @@ -0,0 +1,209 @@ +package NGCP::Panel::Controller::API::PeeringGroups; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +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); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Peering; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +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 => + 'Defines peering groups.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'name', + description => 'Filter for peering group name', + query => { + first => sub { + my $q = shift; + { name => { like => $q } }; + }, + second => sub {}, + }, + }, + { + param => 'description', + description => 'Filter for peering group description', + query => { + first => sub { + my $q = shift; + { description => { like => $q } }; + }, + second => sub {}, + }, + }, + ]}, +); + +with 'NGCP::Panel::Role::API::PeeringGroups'; + +class_has('resource_name', is => 'ro', default => 'peeringgroups'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringgroups/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringgroups'); + +__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); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->all) { + push @embedded, $self->hal_from_item($c, $item, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->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', $c->request->path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->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 POST :Allow { + my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + + my $form = $self->get_form($c); + + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + ); + $resource = $form->custom_get_values; + last unless $resource; + my $item; + my $dup_item = $c->model('DB')->resultset('voip_peer_groups')->find({ + name => $resource->{name}, + }); + if($dup_item) { + $c->log->error("peering group with name '$$resource{name}' already exists"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Peering group with this name already exists"); + last; + } + + try { + $item = $c->model('DB')->resultset('voip_peer_groups')->create($resource); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + } catch($e) { + $c->log->error("failed to create peering group: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create peering group."); + last; + } + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringGroupsItem.pm b/lib/NGCP/Panel/Controller/API/PeeringGroupsItem.pm new file mode 100644 index 0000000000..8343bc591a --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringGroupsItem.pm @@ -0,0 +1,201 @@ +package NGCP::Panel::Controller::API::PeeringGroupsItem; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::PeeringGroups'; + +class_has('resource_name', is => 'ro', default => 'peeringgroups'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringgroups/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringgroups'); + +__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 } + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +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, peeringgroup => $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"|; + s/rel=self/rel="item self"/; + $_ + } $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 PATCH :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ); + last unless $json; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringgroup => $item); + my $old_resource = { $item->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $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, $item, $form); + 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 PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringgroup => $item); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $item->get_inflated_columns }; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $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, $item, $form); + 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 DELETE :Allow { + my ($self, $c, $id) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringgroup => $item); + + foreach my $p ($item->voip_peer_hosts->all) { + $p->voip_peer_preferences->delete_all; + $p->delete; + } + $item->delete; + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringRules.pm b/lib/NGCP/Panel/Controller/API/PeeringRules.pm new file mode 100644 index 0000000000..4206aa8fcc --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringRules.pm @@ -0,0 +1,220 @@ +package NGCP::Panel::Controller::API::PeeringRules; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +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); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Peering; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +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 => + 'Defines peering groups.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'group_id', + description => 'Filter for peering rule group', + query => { + first => sub { + my $q = shift; + { group_id => $q }; + }, + second => sub {}, + }, + }, + { + param => 'description', + description => 'Filter for peering rules description', + query => { + first => sub { + my $q = shift; + { description => { like => $q } }; + }, + second => sub {}, + }, + }, + { + param => 'enabled', + description => 'Filter for peering rules enabled flag', + query => { + first => sub { + my $q = shift; + { enabled => $q }; + }, + second => sub {}, + }, + }, + ]}, +); + +with 'NGCP::Panel::Role::API::PeeringRules'; + +class_has('resource_name', is => 'ro', default => 'peeringrules'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringrules/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringrules'); + +__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); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->all) { + push @embedded, $self->hal_from_item($c, $item, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->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', $c->request->path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->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 POST :Allow { + my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + my $item; + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + exceptions => [qw/group_id/], + ); + my $dup_item = $c->model('DB')->resultset('voip_peer_rules')->find({ + group_id => $resource->{group_id}, + callee_pattern => $resource->{callee_pattern}, + caller_pattern => $resource->{caller_pattern}, + callee_prefix => $resource->{callee_prefix}, + }); + if($dup_item) { + $c->log->error("peering rule already exists"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "peering rule already exists"); + return; + } + + try { + $item = $c->model('DB')->resultset('voip_peer_rules')->create($resource); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + } catch($e) { + $c->log->error("failed to create peering rule: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create peering rule."); + last; + } + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringRulesItem.pm b/lib/NGCP/Panel/Controller/API/PeeringRulesItem.pm new file mode 100644 index 0000000000..75d2330997 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringRulesItem.pm @@ -0,0 +1,196 @@ +package NGCP::Panel::Controller::API::PeeringRulesItem; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::PeeringRules'; + +class_has('resource_name', is => 'ro', default => 'peeringrules'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringrules/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringrules'); + +__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 } + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +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, peeringrule => $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"|; + s/rel=self/rel="item self"/; + $_ + } $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 PATCH :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ); + last unless $json; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringrule => $item); + my $old_resource = { $item->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $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, $item, $form); + 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 PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringrule => $item); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $item->get_inflated_columns }; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $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, $item, $form); + 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 DELETE :Allow { + my ($self, $c, $id) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringrule => $item); + $item->delete; + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringServerPreferenceDefs.pm b/lib/NGCP/Panel/Controller/API/PeeringServerPreferenceDefs.pm new file mode 100644 index 0000000000..b44abbeb33 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringServerPreferenceDefs.pm @@ -0,0 +1,107 @@ +package NGCP::Panel::Controller::API::PeeringServerPreferenceDefs; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +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); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Preferences; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +use JSON::Types qw(); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::CheckTrailingSlash; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API'; + +class_has('resource_name', is => 'ro', default => 'peeringserverpreferencedefs'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringserverpreferencedefs/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringserverpreferencedefs'); + +__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); +} + +sub GET :Allow { + my ($self, $c) = @_; + { + my @links; + 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', $self->dispatch_path)); + + my $hal = Data::HAL->new( + links => [@links], + ); + my $resource = NGCP::Panel::Utils::Preferences::api_preferences_defs( c => $c, preferences_group => 'peer_pref' ); + $hal->resource($resource); + + 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); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringServerPreferences.pm b/lib/NGCP/Panel/Controller/API/PeeringServerPreferences.pm new file mode 100644 index 0000000000..d938b53b4c --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringServerPreferences.pm @@ -0,0 +1,131 @@ +package NGCP::Panel::Controller::API::PeeringServerPreferences; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +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); +use NGCP::Panel::Utils::DateTime; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +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 => + 'Specifies certain properties (preferences) for a Peering servers. The full list of properties can be obtained via PeeringSserverPreferenceDefs.' +); + +with 'NGCP::Panel::Role::API::Preferences'; + +class_has('resource_name', is => 'ro', default => 'peeringserverpreferences'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringserverpreferences/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringserverpreferences'); + +__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); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $container_type = 'peerings'; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $container_items = $self->item_rs($c, $container_type); + (my $total_count, $container_items) = $self->paginate_order_collection($c, $container_items); + my (@embedded, @links); + for my $container_item ($container_items->all) { + push @embedded, $self->hal_from_item($c, $container_item, $container_type); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('%s%d', $self->dispatch_path, $container_items->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', $self->dispatch_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); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringServerPreferencesItem.pm b/lib/NGCP/Panel/Controller/API/PeeringServerPreferencesItem.pm new file mode 100644 index 0000000000..629688bb06 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringServerPreferencesItem.pm @@ -0,0 +1,240 @@ +package NGCP::Panel::Controller::API::PeeringServerPreferencesItem; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +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); +use NGCP::Panel::Utils::ValidateJSON qw(); +use NGCP::Panel::Utils::DateTime; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::Preferences'; + +class_has('resource_name', is => 'ro', default => 'peeringserverpreferences'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringserverpreferences/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringserverpreferences'); + +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); +} + +sub GET :Allow { + my ($self, $c, $id) = @_; + { + last unless $self->valid_id($c, $id); + my $container_type = "peerings"; + my $preferences_type = "peeringserverpreference"; + my $container_item = $self->item_by_id($c, $id, $container_type); + last unless $self->resource_exists($c, $preferences_type => $container_item); + + my $hal = $self->hal_from_item($c, $container_item, $container_type); + + 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"|; + s/rel=self/rel="item self"/; + $_ + } $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 PATCH :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ops => [qw/add replace remove copy/], + ); + last unless $json; + + my $container_type = "peerings"; + my $preferences_type = "peeringserverpreference"; + my $container_item = $self->item_by_id($c, $id, $container_type); + last unless $self->resource_exists($c, $preferences_type => $container_item); + my $old_resource = $self->get_resource($c, $container_item, $container_type); + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + # last param is "no replace" to NOT delete existing prefs + # for proper PATCH behavior + $container_item = $self->update_item($c, $container_item, $old_resource, $resource, 0, $container_type); + last unless $container_item; + + my $hal = $self->hal_from_item($c, $container_item, $container_type); + last unless $self->add_update_journal_item_hal($c,$hal); + + $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, $container_item, $container_type); + 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 PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $container_type = "peerings"; + my $preferences_type = "peeringserverpreference"; + my $container_item = $self->item_by_id($c, $id, $container_type); + # TODO: systemcontact? + last unless $self->resource_exists($c, $preferences_type => $container_item); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = $self->get_resource($c, $container_item, $container_type); + + # last param is "replace" to delete all existing prefs + # for proper PUT behavior + $container_item = $self->update_item($c, $container_item, $old_resource, $resource, 1, $container_type); + last unless $container_item; + + my $hal = $self->hal_from_item($c, $container_item, $container_type); + last unless $self->add_update_journal_item_hal($c,$hal); + + $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, $container_item, $container_type); + 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); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringServers.pm b/lib/NGCP/Panel/Controller/API/PeeringServers.pm new file mode 100644 index 0000000000..b9d08c0a7d --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringServers.pm @@ -0,0 +1,239 @@ +package NGCP::Panel::Controller::API::PeeringServers; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +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); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::Peering; +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +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 => + 'Defines peering groups.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'group_id', + description => 'Filter for peering server group', + query => { + first => sub { + my $q = shift; + { group_id => $q }; + }, + second => sub {}, + }, + }, + { + param => 'name', + description => 'Filter for peering server name', + query => { + first => sub { + my $q = shift; + { name => { like => $q } }; + }, + second => sub {}, + }, + }, + { + param => 'host', + description => 'Filter for peering server host', + query => { + first => sub { + my $q = shift; + { host => { like => $q } }; + }, + second => sub {}, + }, + }, + { + param => 'ip', + description => 'Filter for peering server ip', + query => { + first => sub { + my $q = shift; + { host => { like => $q } }; + }, + second => sub {}, + }, + }, + { + param => 'enabled', + description => 'Filter for peering server enabled flag', + query => { + first => sub { + my $q = shift; + { enabled => $q }; + }, + second => sub {}, + }, + }, ]}, +); + +with 'NGCP::Panel::Role::API::PeeringServers'; + +class_has('resource_name', is => 'ro', default => 'peeringservers'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringservers/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringservers'); + +__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); +} + +sub GET :Allow { + my ($self, $c) = @_; + my $page = $c->request->params->{page} // 1; + my $rows = $c->request->params->{rows} // 10; + { + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); + my (@embedded, @links); + my $form = $self->get_form($c); + for my $item ($items->all) { + push @embedded, $self->hal_from_item($c, $item, $form); + push @links, Data::HAL::Link->new( + relation => 'ngcp:'.$self->resource_name, + href => sprintf('/%s%d', $c->request->path, $item->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', $c->request->path, $page, $rows)); + if(($total_count / $rows) > $page ) { + push @links, Data::HAL::Link->new(relation => 'next', href => sprintf('/%s?page=%d&rows=%d', $c->request->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 POST :Allow { + my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $resource = $self->get_valid_post_data( + c => $c, + media_type => 'application/json', + ); + last unless $resource; + my $item; + + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + exceptions => [qw/group_id/], + ); + my $dup_item = $c->model('DB')->resultset('voip_peer_hosts')->find({ + name => $resource->{name}, + }); + if($dup_item) { + $c->log->error("peering server with name '$$resource{name}' already exists"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "peering server with this name already exists"); + return; + } + + try { + $item = $c->model('DB')->resultset('voip_peer_hosts')->create($resource); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + } catch($e) { + $c->log->error("failed to create peering server: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create peering server."); + last; + } + + $guard->commit; + + $c->response->status(HTTP_CREATED); + $c->response->header(Location => sprintf('/%s%d', $c->request->path, $item->id)); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PeeringServersItem.pm b/lib/NGCP/Panel/Controller/API/PeeringServersItem.pm new file mode 100644 index 0000000000..d15149493e --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PeeringServersItem.pm @@ -0,0 +1,196 @@ +package NGCP::Panel::Controller::API::PeeringServersItem; +use NGCP::Panel::Utils::Generic qw(:all); +use Sipwise::Base; +use Moose; +#use namespace::sweep; +use HTTP::Headers qw(); +use HTTP::Status qw(:constants); +use MooseX::ClassAttribute qw(class_has); +use NGCP::Panel::Utils::DateTime; +use NGCP::Panel::Utils::ValidateJSON qw(); +use Path::Tiny qw(path); +use Safe::Isa qw($_isa); +BEGIN { extends 'Catalyst::Controller::ActionRole'; } +require Catalyst::ActionRole::ACL; +require Catalyst::ActionRole::HTTPMethods; +require Catalyst::ActionRole::RequireSSL; + +with 'NGCP::Panel::Role::API::PeeringServers'; + +class_has('resource_name', is => 'ro', default => 'peeringservers'); +class_has('dispatch_path', is => 'ro', default => '/api/peeringservers/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-peeringservers'); + +__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 } + }, + action_roles => [qw(HTTPMethods)], +); + +sub auto :Private { + my ($self, $c) = @_; + + $self->set_body($c); + $self->log_request($c); +} + +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, peeringserver => $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"|; + s/rel=self/rel="item self"/; + $_ + } $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 PATCH :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $json = $self->get_valid_patch_data( + c => $c, + id => $id, + media_type => 'application/json-patch+json', + ); + last unless $json; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringserver => $item); + my $old_resource = { $item->get_inflated_columns }; + my $resource = $self->apply_patch($c, $old_resource, $json); + last unless $resource; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $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, $item, $form); + 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 PUT :Allow { + my ($self, $c, $id) = @_; + my $guard = $c->model('DB')->txn_scope_guard; + { + my $preference = $self->require_preference($c); + last unless $preference; + + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringserver => $item); + my $resource = $self->get_valid_put_data( + c => $c, + id => $id, + media_type => 'application/json', + ); + last unless $resource; + my $old_resource = { $item->get_inflated_columns }; + + my $form = $self->get_form($c); + $item = $self->update_item($c, $item, $old_resource, $resource, $form); + last unless $item; + + $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, $item, $form); + 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 DELETE :Allow { + my ($self, $c, $id) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, peeringserver => $item); + $item->delete; + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +no Moose; +1; + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/RewriteRules.pm b/lib/NGCP/Panel/Controller/API/RewriteRules.pm index 3d49459f64..c4bc52121f 100644 --- a/lib/NGCP/Panel/Controller/API/RewriteRules.pm +++ b/lib/NGCP/Panel/Controller/API/RewriteRules.pm @@ -49,6 +49,17 @@ class_has 'query_params' => ( second => sub {}, }, }, + { + param => 'reseller_id', + description => 'Filter for rules belonging to a specific reseller.', + query => { + first => sub { + my $q = shift; + return { set_id => $q }; + }, + second => sub {}, + }, + }, ]}, ); diff --git a/lib/NGCP/Panel/Controller/API/Root.pm b/lib/NGCP/Panel/Controller/API/Root.pm index b2eee61d90..dafd226637 100644 --- a/lib/NGCP/Panel/Controller/API/Root.pm +++ b/lib/NGCP/Panel/Controller/API/Root.pm @@ -54,6 +54,7 @@ sub GET : Allow { "SubscriberPreferenceDefs" => 1, "CustomerPreferenceDefs" => 1, "ProfilePreferenceDefs" => 1, + "PeeringServerPreferenceDefs" => 1, }; my @colls = $self->get_collections; diff --git a/lib/NGCP/Panel/Controller/Peering.pm b/lib/NGCP/Panel/Controller/Peering.pm index 533e5bdd94..659f9d472e 100644 --- a/lib/NGCP/Panel/Controller/Peering.pm +++ b/lib/NGCP/Panel/Controller/Peering.pm @@ -2,17 +2,16 @@ package NGCP::Panel::Controller::Peering; use NGCP::Panel::Utils::Generic qw(:all); use Sipwise::Base; - BEGIN { use base 'Catalyst::Controller'; } -use NGCP::Panel::Form::PeeringGroup; -use NGCP::Panel::Form::PeeringRule; -use NGCP::Panel::Form::PeeringServer; +use NGCP::Panel::Form::Peering::Group; +use NGCP::Panel::Form::Peering::Rule; +use NGCP::Panel::Form::Peering::Server; use NGCP::Panel::Utils::DialogicImg; use NGCP::Panel::Utils::Message; use NGCP::Panel::Utils::Navigation; use NGCP::Panel::Utils::Preferences; -use NGCP::Panel::Utils::XMLDispatcher; +use NGCP::Panel::Utils::Peering; sub auto :Does(ACL) :ACLDetachTo('/denied_page') :AllowedRole(admin) { my ($self, $c) = @_; @@ -108,7 +107,7 @@ sub edit :Chained('base') :PathPart('edit') { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::PeeringGroup->new; + my $form = NGCP::Panel::Form::Peering::Group->new; my $params = { $c->stash->{group_result}->get_inflated_columns }; $params->{contract}{id} = delete $params->{peering_contract_id}; $params = merge($params, $c->session->{created_objects}); @@ -125,7 +124,7 @@ sub edit :Chained('base') :PathPart('edit') { if($posted && $form->validated) { try { $c->stash->{group_result}->update($form->custom_get_values); - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); delete $c->session->{created_objects}->{contract}; NGCP::Panel::Utils::Message::info( c => $c, @@ -156,7 +155,7 @@ sub delete :Chained('base') :PathPart('delete') { $p->delete; } $c->stash->{group_result}->delete; - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, data => { $c->stash->{group_result}->get_inflated_columns }, @@ -177,7 +176,7 @@ sub create :Chained('group_list') :PathPart('create') :Args(0) { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::PeeringGroup->new; + my $form = NGCP::Panel::Form::Peering::Group->new; my $params = {}; $params = merge($params, $c->session->{created_objects}); $form->process( @@ -195,7 +194,7 @@ sub create :Chained('group_list') :PathPart('create') :Args(0) { try { $c->model('DB')->resultset('voip_peer_groups')->create( $formdata ); - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); delete $c->session->{created_objects}->{contract}; NGCP::Panel::Utils::Message::info( c => $c, @@ -242,7 +241,7 @@ sub servers_create :Chained('servers_list') :PathPart('create') :Args(0) { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::PeeringServer->new(ctx => $c); + my $form = NGCP::Panel::Form::Peering::Server->new(ctx => $c); $form->process( posted => $posted, params => $c->request->params, @@ -266,7 +265,7 @@ sub servers_create :Chained('servers_list') :PathPart('create') :Args(0) { enabled => $form->values->{enabled}, }; my $server = $c->stash->{group_result}->voip_peer_hosts->create($dbvalues); - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, desc => $c->loc('Peering server successfully created'), @@ -323,7 +322,7 @@ sub servers_edit :Chained('servers_base') :PathPart('edit') :Args(0) { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::PeeringServer->new(ctx => $c); + my $form = NGCP::Panel::Form::Peering::Server->new(ctx => $c); $form->process( posted => $posted, params => $c->request->params, @@ -338,7 +337,7 @@ sub servers_edit :Chained('servers_base') :PathPart('edit') :Args(0) { if($posted && $form->validated) { try { $c->stash->{server_result}->update($form->values); - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, desc => $c->loc('Peering server successfully updated'), @@ -366,7 +365,7 @@ sub servers_delete :Chained('servers_base') :PathPart('delete') :Args(0) { try { $c->stash->{server_result}->delete; - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, data => { $c->stash->{server_result}->get_inflated_columns }, @@ -567,7 +566,7 @@ sub rules_create :Chained('rules_list') :PathPart('create') :Args(0) { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::PeeringRule->new; + my $form = NGCP::Panel::Form::Peering::Rule->new; $form->process( posted => $posted, params => $c->request->params, @@ -582,7 +581,7 @@ sub rules_create :Chained('rules_list') :PathPart('create') :Args(0) { try { $form->values->{callee_prefix} //= ''; $c->stash->{group_result}->voip_peer_rules->create($form->values); - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, desc => $c->loc('Peering rule successfully created'), @@ -639,7 +638,7 @@ sub rules_edit :Chained('rules_base') :PathPart('edit') :Args(0) { my ($self, $c) = @_; my $posted = ($c->request->method eq 'POST'); - my $form = NGCP::Panel::Form::PeeringRule->new; + my $form = NGCP::Panel::Form::Peering::Rule->new; $form->process( posted => $posted, params => $c->request->params, @@ -655,7 +654,7 @@ sub rules_edit :Chained('rules_base') :PathPart('edit') :Args(0) { try { $form->values->{callee_prefix} //= ''; $c->stash->{rule_result}->update($form->values); - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, desc => $c->loc('Peering rule successfully changed'), @@ -683,7 +682,7 @@ sub rules_delete :Chained('rules_base') :PathPart('delete') :Args(0) { try { $c->stash->{rule_result}->delete; - $self->_sip_lcr_reload($c); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); NGCP::Panel::Utils::Message::info( c => $c, data => { $c->stash->{rule_result}->get_inflated_columns }, @@ -700,19 +699,6 @@ sub rules_delete :Chained('rules_base') :PathPart('delete') :Args(0) { return; } -sub _sip_lcr_reload { - my ($self, $c) = @_; - my $dispatcher = NGCP::Panel::Utils::XMLDispatcher->new; - $dispatcher->dispatch($c, "proxy-ng", 1, 1, < - -lcr.reload - - -EOF - - return 1; -} __PACKAGE__->meta->make_immutable; @@ -841,12 +827,6 @@ Show a modal to edit a peering rule. Delete a peering rule. -=head2 _sip_lcr_reload - -This is ported from ossbss. - -Reloads lcr cache of sip proxies. - =head1 AUTHOR Gerhard Jungwirth C<< >> diff --git a/lib/NGCP/Panel/Form/PeeringGroup.pm b/lib/NGCP/Panel/Form/Peering/Group.pm similarity index 96% rename from lib/NGCP/Panel/Form/PeeringGroup.pm rename to lib/NGCP/Panel/Form/Peering/Group.pm index 4e46c1bb98..ec5c5654f7 100644 --- a/lib/NGCP/Panel/Form/PeeringGroup.pm +++ b/lib/NGCP/Panel/Form/Peering/Group.pm @@ -1,4 +1,4 @@ -package NGCP::Panel::Form::PeeringGroup; +package NGCP::Panel::Form::Peering::Group; use HTML::FormHandler::Moose; extends 'HTML::FormHandler'; @@ -72,7 +72,7 @@ sub custom_get_values { =head1 NAME -NGCP::Panel::Form::PeeringGroup +NGCP::Panel::Form::Peering::Group =head1 DESCRIPTION diff --git a/lib/NGCP/Panel/Form/PeeringRule.pm b/lib/NGCP/Panel/Form/Peering/Rule.pm similarity index 96% rename from lib/NGCP/Panel/Form/PeeringRule.pm rename to lib/NGCP/Panel/Form/Peering/Rule.pm index 3895d0d255..0846566fae 100644 --- a/lib/NGCP/Panel/Form/PeeringRule.pm +++ b/lib/NGCP/Panel/Form/Peering/Rule.pm @@ -1,4 +1,4 @@ -package NGCP::Panel::Form::PeeringRule; +package NGCP::Panel::Form::Peering::Rule; use Sipwise::Base; use HTML::FormHandler::Moose; extends 'HTML::FormHandler'; @@ -81,7 +81,7 @@ __END__ =head1 NAME -NGCP::Panel::Form::PeeringRule +NGCP::Panel::Form::Peering::Rule =head1 DESCRIPTION diff --git a/lib/NGCP/Panel/Form/Peering/RuleAPI.pm b/lib/NGCP/Panel/Form/Peering/RuleAPI.pm new file mode 100644 index 0000000000..70728ab95f --- /dev/null +++ b/lib/NGCP/Panel/Form/Peering/RuleAPI.pm @@ -0,0 +1,45 @@ +package NGCP::Panel::Form::Peering::RuleAPI; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::Peering::Rule'; + +has_field 'group_id' => ( + type => 'PosInteger', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The peering group this rule belongs to.'] + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/group_id callee_prefix callee_pattern caller_pattern description enabled/], +); + +1; +__END__ + +=head1 NAME + +NGCP::Panel::Form::Peering::RuleAPI + +=head1 DESCRIPTION + +- + +=head1 METHODS + +=head1 AUTHOR + +Irina Peshinskaya + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/PeeringServer.pm b/lib/NGCP/Panel/Form/Peering/Server.pm similarity index 94% rename from lib/NGCP/Panel/Form/PeeringServer.pm rename to lib/NGCP/Panel/Form/Peering/Server.pm index f7d3a60301..0d69047fa9 100644 --- a/lib/NGCP/Panel/Form/PeeringServer.pm +++ b/lib/NGCP/Panel/Form/Peering/Server.pm @@ -1,4 +1,4 @@ -package NGCP::Panel::Form::PeeringServer; +package NGCP::Panel::Form::Peering::Server; use Sipwise::Base; use HTML::FormHandler::Moose; extends 'HTML::FormHandler'; @@ -125,14 +125,19 @@ sub validate_via_route { $field->add_error("Invalid SIP URI, must be (comma-separated) SIP URI(s) in form sip:ip:port"); } } - +#sub validate { +# my ($self) = @_; +# my $c = $self->ctx; +# return unless $c; +# my $model = $c-> +#} 1; __END__ =head1 NAME -NGCP::Panel::Form::PeeringServer +NGCP::Panel::Form::Peering::Server =head1 DESCRIPTION diff --git a/lib/NGCP/Panel/Form/Peering/ServerAPI.pm b/lib/NGCP/Panel/Form/Peering/ServerAPI.pm new file mode 100644 index 0000000000..36b2caa31c --- /dev/null +++ b/lib/NGCP/Panel/Form/Peering/ServerAPI.pm @@ -0,0 +1,44 @@ +package NGCP::Panel::Form::Peering::ServerAPI; +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::Peering::Server'; + +has_field 'group_id' => ( + type => 'PosInteger', + required => 1, + element_attr => { + rel => ['tooltip'], + title => ['The peering group this server belongs to.'] + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/group_id name ip host port transport weight via_route enabled/], +); + +1; +__END__ + +=head1 NAME + +NGCP::Panel::Form::Peering::ServerAPI + +=head1 DESCRIPTION + +- + +=head1 METHODS + +=head1 AUTHOR + +Irina Peshinskaya + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/PeeringGroups.pm b/lib/NGCP/Panel/Role/API/PeeringGroups.pm new file mode 100644 index 0000000000..b7ae950e12 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/PeeringGroups.pm @@ -0,0 +1,95 @@ +package NGCP::Panel::Role::API::PeeringGroups; +use NGCP::Panel::Utils::Generic qw(:all); +use Moose::Role; +use Sipwise::Base; +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 NGCP::Panel::Form::Peering::Group; +use NGCP::Panel::Utils::Peering; + +sub item_rs { + my ($self, $c) = @_; + my $item_rs = $c->model('DB')->resultset('voip_peer_groups'); + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::Peering::Group->new; +} + +sub hal_from_item { + my ($self, $c, $item, $form) = @_; + my %resource = $item->get_inflated_columns; + $resource{contract_id} = delete $resource{peering_contract_id}; + 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)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + + $self->validate_form( + c => $c, + resource => \%resource, + form => $form, + run => 0, + ); + + $resource{id} = int($item->id); + $hal->resource({%resource}); + return $hal; +} + +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) = @_; + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + ); + $resource = $form->custom_get_values; + last unless $resource; + + my $dup_item = $c->model('DB')->resultset('voip_peer_groups')->find({ + name => $resource->{name}, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("peering group with name '$$resource{name}' already exists"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "peering group with this name already exists"); + return; + } + + $item->update($resource); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/PeeringRules.pm b/lib/NGCP/Panel/Role/API/PeeringRules.pm new file mode 100644 index 0000000000..fb14074788 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/PeeringRules.pm @@ -0,0 +1,98 @@ +package NGCP::Panel::Role::API::PeeringRules; +use NGCP::Panel::Utils::Generic qw(:all); +use Moose::Role; +use Sipwise::Base; +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 NGCP::Panel::Form::Peering::RuleAPI; +use NGCP::Panel::Utils::Peering; + +sub item_rs { + my ($self, $c) = @_; + my $item_rs = $c->model('DB')->resultset('voip_peer_rules'); + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::Peering::RuleAPI->new; +} + +sub hal_from_item { + my ($self, $c, $item, $form) = @_; + my %resource = $item->get_inflated_columns; + + 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:peeringgroups', href => sprintf("/api/peeringgroups/%d", $resource{group_id})), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + + $self->validate_form( + c => $c, + resource => \%resource, + form => $form, + exceptions => [qw/group_id/], + run => 0, + ); + + $resource{id} = int($item->id); + $hal->resource({%resource}); + return $hal; +} + +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) = @_; + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + exceptions => [qw/group_id/], + ); + my $dup_item = $c->model('DB')->resultset('voip_peer_rules')->find({ + group_id => $resource->{group_id}, + callee_pattern => $resource->{callee_pattern}, + caller_pattern => $resource->{caller_pattern}, + callee_prefix => $resource->{callee_prefix}, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("peering rule already exists"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "peering rule already exists"); + return; + } + + $item->update($resource); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/PeeringServers.pm b/lib/NGCP/Panel/Role/API/PeeringServers.pm new file mode 100644 index 0000000000..db79147e99 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/PeeringServers.pm @@ -0,0 +1,96 @@ +package NGCP::Panel::Role::API::PeeringServers; +use NGCP::Panel::Utils::Generic qw(:all); +use Moose::Role; +use Sipwise::Base; +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 NGCP::Panel::Form::Peering::ServerAPI; +use NGCP::Panel::Utils::Peering; + +sub item_rs { + my ($self, $c) = @_; + my $item_rs = $c->model('DB')->resultset('voip_peer_hosts'); + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::Peering::ServerAPI->new; +} + +sub hal_from_item { + my ($self, $c, $item, $form) = @_; + my %resource = $item->get_inflated_columns; + + 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:peeringgroups', href => sprintf("/api/peeringgroups/%d", $resource{group_id})), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + $form //= $self->get_form($c); + + $self->validate_form( + c => $c, + resource => \%resource, + form => $form, + run => 0, + exceptions => [qw/group_id/], + ); + + $resource{id} = int($item->id); + $hal->resource({%resource}); + return $hal; +} + +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) = @_; + + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + exceptions => [qw/group_id/], + ); + + my $dup_item = $c->model('DB')->resultset('voip_peer_hosts')->find({ + name => $resource->{name}, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("peering server with name '$$resource{name}' already exists"); # TODO: user, message, trace, ... + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "peering server with this name already exists"); + return; + } + + $item->update($resource); + NGCP::Panel::Utils::Peering::_sip_lcr_reload(c => $c); + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Utils/Peering.pm b/lib/NGCP/Panel/Utils/Peering.pm new file mode 100644 index 0000000000..a951fb4d17 --- /dev/null +++ b/lib/NGCP/Panel/Utils/Peering.pm @@ -0,0 +1,50 @@ +package NGCP::Panel::Utils::Peering; +use NGCP::Panel::Utils::XMLDispatcher; + +use strict; +use warnings; + +sub _sip_lcr_reload { + my(%params) = @_; + my($c) = @params{qw/c/}; + my $dispatcher = NGCP::Panel::Utils::XMLDispatcher->new; + $dispatcher->dispatch($c, "proxy-ng", 1, 1, < + +lcr.reload + + +EOF + + return 1; +} + +1; + +=head1 NAME + +NGCP::Panel::Utils::Peering + +=head1 DESCRIPTION + +A temporary helper to manipulate peerings related data + +=head1 METHODS + +=head2 _sip_lcr_reload + +This is ported from ossbss. + +Reloads lcr cache of sip proxies. + +=head1 AUTHOR + +Irina Peshinskaya + +=head1 LICENSE + +This library is free software. You can redistribute it and/or modify +it under the same terms as Perl itself. + +=cut +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-ncoslevels.t b/t/api-rest/api-ncoslevels.t new file mode 100644 index 0000000000..1ff6718a94 --- /dev/null +++ b/t/api-rest/api-ncoslevels.t @@ -0,0 +1,41 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'ncoslevels', +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + ncoslevels => { + data => { + reseller_id => sub { return shift->get_id('resellers',@_); }, + level => 'api_test ncos '.time(), + mode => 'whitelist',#blacklist + description => 'api_test ncos level description', + local_ac => '1',#out + }, + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('ncoslevels')); + +$test_machine->form_data_item( ); +# create 3 new field pbx devices from DATA_ITEM +$test_machine->check_create_correct( 3, sub{ $_[0]->{level} .= $_[1]->{i}; } ); +$test_machine->check_get2put(); +$test_machine->check_bundle(); +$test_machine->clear_test_data_all(); + +done_testing; + +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-peeringgroups.t b/t/api-rest/api-peeringgroups.t new file mode 100644 index 0000000000..3e02d8af3d --- /dev/null +++ b/t/api-rest/api-peeringgroups.t @@ -0,0 +1,47 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'peeringgroups', +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + peeringgroups => { + data => { + name => 'test_api peering group', + priority => '1', + description => 'test_api peering group', + contract_id => sub { return shift->get_id('contracts',@_); },, + }, + query => ['name'], + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('peeringgroups')); + +$test_machine->form_data_item( ); +# create 3 new field pbx devices from DATA_ITEM +$test_machine->check_create_correct( 3, sub{ $_[0]->{name} .= $_[1]->{i}; } ); +{ + my $data = $test_machine->DATA_ITEM; + $data->{name} .= 1; + my ($res,$result_item,$req) = $test_machine->request_post($data); + $test_machine->http_code_msg(422, "POST same peering group code again", $res, $result_item); +} +$test_machine->check_get2put(); +$test_machine->check_bundle(); +$test_machine->clear_test_data_all(); + +done_testing; + +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-peeringrules.t b/t/api-rest/api-peeringrules.t new file mode 100644 index 0000000000..7c7a785b8a --- /dev/null +++ b/t/api-rest/api-peeringrules.t @@ -0,0 +1,49 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'peeringrules', + embedded_resources => [qw/peeringgroups/] +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + peeringrules => { + data => { + group_id => sub { return shift->get_id('peeringgroups',@_); }, + callee_prefix => '333', + callee_pattern => '^111$', + caller_pattern => '^222$', + description => 'api_test peering rule', + enabled => '1', + } + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('peeringrules')); + +$test_machine->form_data_item( ); +# create 3 new field pbx devices from DATA_ITEM +$test_machine->check_create_correct( 3, sub{ $_[0]->{callee_prefix} .= $_[1]->{i}; } ); +{ + my $data = $test_machine->DATA_ITEM; + $data->{callee_prefix} .= 1; + my ($res,$result_item,$req) = $test_machine->request_post($data); + $test_machine->http_code_msg(422, "POST same peering rule code again", $res, $result_item); +} +$test_machine->check_get2put(); +$test_machine->check_bundle(); +$test_machine->clear_test_data_all(); + +done_testing; + +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-peeringservers.t b/t/api-rest/api-peeringservers.t new file mode 100644 index 0000000000..ef2921a7d0 --- /dev/null +++ b/t/api-rest/api-peeringservers.t @@ -0,0 +1,54 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'peeringservers', + embedded_resources => [qw/peeringgroups/] +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + peeringservers => { + data => { + group_id => sub { return shift->get_id('peeringgroups',@_); }, + name => 'test_api peering host', + ip => '1.1.1.1', + host => 'test-api.com', + port => '1025', + transport => '1', + weight => '1', + via_route => '', + via_lb => '', + enabled => '1', + }, + query => ['group_id','name'], + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('peeringservers')); + +$test_machine->form_data_item( ); +# create 3 new field pbx devices from DATA_ITEM +$test_machine->check_create_correct( 3, sub{ $_[0]->{name} .= $_[1]->{i}; } ); +{ + my $data = $test_machine->DATA_ITEM; + $data->{name} .= 1; + my ($res,$result_item,$req) = $test_machine->request_post($data); + $test_machine->http_code_msg(422, "POST same peering server name again", $res, $result_item); +} +$test_machine->check_get2put(); +$test_machine->check_bundle(); +$test_machine->clear_test_data_all(); + +done_testing; + +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-preferences.t b/t/api-rest/api-preferences.t new file mode 100644 index 0000000000..00c7360aee --- /dev/null +++ b/t/api-rest/api-preferences.t @@ -0,0 +1,139 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; +use JSON; +use Clone qw/clone/; +use feature "state"; + + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'preferences', +); +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + preferences => { + data => { + peeringserver_id => sub { return shift->get_id('peeringservers',@_); }, + customer_id => sub { return shift->get_id('customers',@_); }, + subscriber_id => sub { return shift->get_id('subscribers',@_); }, + domain_id => sub { return shift->get_id('domains',@_); }, + profile_id => sub { return shift->get_id('subscriberprofiles',@_); }, + + rewriteruleset_id => sub { return shift->get_id('rewriterulesets',@_); }, + soundset_id => sub { return shift->get_id('soundsets',@_); }, + ncoslevel_id => sub { return shift->get_id('ncoslevels',@_); }, + }, + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('preferences')); +$test_machine->form_data_item( ); + +my @apis = qw/subscriber domain peeringserver customer profile/; +#my @apis = qw/peeringserver/; + +foreach my $api (@apis){ + my $preferences_old; + my $preferences_put; + my $index = $api.'_id'; + my ($preferences) = {'uri' => '/api/'.$api.'preferences/'.$test_machine->DATA_ITEM->{$index}}; + (undef, $preferences_old) = $test_machine->check_item_get($preferences->{uri}); + #$preferences->{content} = $preferences_old; + + my $api_test_machine = Test::Collection->new( + name => $api.'preferences', + ); + $api_test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; + $api_test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; + my $defs = $api_test_machine->get_item_hal($api.'preferencedefs'); + delete $defs->{content}->{_links}; + foreach my $preference_name(keys %{$defs->{content}}){ + my $preference = $defs->{content}->{$preference_name}; + $preference->{name} = $preference_name; + #if($preference->{read_only}){ + # next; + #} + my $value; + if('boolean' eq $preference->{data_type}){ + $value = JSON::true; + }elsif('enum' eq $preference->{data_type}){ + my @values = @{$preference->{enum_values}}; + if(@values){ + if($#values > 0){ + #take second value from enum if exists + $value = $values[1]->{value}; + } + } + #foreach my $preference_enum_value(@{$preference->{enum_values}}){ + # + #} + }elsif('string' eq $preference->{data_type}){ + $value = get_preference_existen_value($preference) // "test_api preference string"; + }elsif('int' eq $preference->{data_type}){ + $value = get_preference_existen_value($preference) // 33; + }else{ + die("unknown data type: ".$preference->{data_type}." for $preference_name;\n"); + } + if($value && 'no_process' ne $value){ + if($preference->{max_occur} > 0 ){ + $preferences->{content}->{$preference_name} = 1 < $preference->{max_occur} ? [$value] : $value ; + } + }else{ + #print "Undefined value for preference: $api:$preference_name;\n"; + #print Dumper $preference; + } + } + #(undef, $preferences_put->{content}) = $test_machine->request_put($preferences->{content},$preferences->{uri}); + #we don't check read_only flag when update preferences? + (undef, $preferences_put->{content}) = $test_machine->check_put2get({data_in=>$preferences->{content},uri=>$preferences->{uri}},undef, 1); + (undef, $preferences_put->{content}) = $test_machine->request_put($preferences_old,$preferences->{uri}); +} + +done_testing; + + +#----------------- aux +sub get_preference_existen_value{ + my $preference = shift; + my $res; + if($preference->{name}=~/^rewrite_rule_set$/){ + $res = $fake_data->{data}->{rewriterulesets}->{data}->{name}; + }elsif($preference->{name}=~/^(adm_)?ncos$/){ + $res = $fake_data->{data}->{ncoslevels}->{data}->{level}; + }elsif($preference->{name}=~/^(contract_)?sound_set$/){ + $res = $fake_data->{data}->{soundsets}->{data}->{name}; + }elsif($preference->{name}=~/^(man_)?allowed_ips_grp$/){ + $res= 'no_process'; + } + return $res; +} + +__DATA__ + +'lbrtp_set' => { + 'enum_values' => [ + { + 'value' => undef, + 'default_val' => $VAR1->{'mobile_push_expiry'}{'read_only'}, + 'label' => 'None' + }, + { + 'value' => '50', + 'default_val' => $VAR1->{'mobile_push_expiry'}{'read_only'}, + 'label' => 'default' + } + ], + 'data_type' => 'enum', + 'read_only' => $VAR1->{'mobile_push_expiry'}{'read_only'}, + 'max_occur' => 1, + 'label' => 'The cluster set used for SIP lb and RTP', + 'description' => 'Use a particular cluster set of load-balancers for SIP towards this endpoint (only for peers, as for subscribers it is defined by Path during registration) and of RTP relays (both peers and subscribers).' +}, + +# vim: set tabstop=4 expandtab: diff --git a/t/api-rest/api-root.t b/t/api-rest/api-root.t index aeba0f2a40..9fa5d15c0c 100644 --- a/t/api-rest/api-root.t +++ b/t/api-rest/api-root.t @@ -88,6 +88,11 @@ $ua->credentials($netloc, "api_admin_http", $user, $pass); pbxdevicemodelimages => 1, pbxdeviceprofiles => 1, pbxdevices => 1, + peeringgroups => 1, + peeringrules => 1, + peeringserverpreferences => 1, + peeringserverpreferencedefs => 1, + peeringservers => 1, profilepackages => 1, profilepreferences => 1, profilepreferencedefs => 1, diff --git a/t/api-rest/api-soundsets.t b/t/api-rest/api-soundsets.t new file mode 100644 index 0000000000..c2de42c867 --- /dev/null +++ b/t/api-rest/api-soundsets.t @@ -0,0 +1,41 @@ +use strict; +use warnings; + +use Test::Collection; +use Test::FakeData; +use Test::More; +use Data::Dumper; + +#init test_machine +my $test_machine = Test::Collection->new( + name => 'soundsets', +); +$test_machine->methods->{collection}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS POST)}; +$test_machine->methods->{item}->{allowed} = {map {$_ => 1} qw(GET HEAD OPTIONS PUT PATCH DELETE)}; + +my $fake_data = Test::FakeData->new; +$fake_data->set_data_from_script({ + soundsets => { + data => { + reseller_id => sub { return shift->get_id('resellers',@_); }, + contract_id => sub { return shift->get_id('customers',@_); }, + name => 'api_test soundset name'.time(), + description => 'api_test soundset description', + contract_default => '1',#0 + }, + }, +}); + +#for item creation test purposes /post request data/ +$test_machine->DATA_ITEM_STORE($fake_data->process('soundsets')); + +$test_machine->form_data_item( ); +# create 3 new sound sets from DATA_ITEM +$test_machine->check_create_correct( 3, sub{ $_[0]->{name} .= $_[1]->{i}; } ); +$test_machine->check_get2put(); +$test_machine->check_bundle(); +$test_machine->clear_test_data_all(); + +done_testing; + +# vim: set tabstop=4 expandtab: diff --git a/t/lib/Test/Collection.pm b/t/lib/Test/Collection.pm index e3650660d1..33dd39d7c2 100644 --- a/t/lib/Test/Collection.pm +++ b/t/lib/Test/Collection.pm @@ -331,9 +331,13 @@ sub get_hal_from_collection{ if(ref $list_collection->{_links}->{$hal_name} eq "HASH") { $reshal = $list_collection; $location = $reshal->{_links}->{$hal_name}->{href}; - } else { + } elsif( $list_collection->{_embedded} && ref $list_collection->{_embedded}->{$hal_name} eq 'ARRAY') { $reshal = $list_collection->{_embedded}->{$hal_name}->[0]; $location = $reshal->{_links}->{self}->{href}; + }elsif( ref $list_collection eq 'HASH' && $list_collection->{_links}->{self}->{href}) { +#preferencedefs collection + $reshal = $list_collection; + $location = $reshal->{_links}->{self}->{href}; } return ($reshal,$location); } @@ -886,7 +890,7 @@ sub check_get2put{ } sub check_put2get{ - my($self, $put_in, $get_in, $nocheck) = @_; + my($self, $put_in, $get_in, $check_cb_or_switch) = @_; my($put_out,$get_out); @@ -903,8 +907,11 @@ sub check_put2get{ delete $get_out->{content}->{_links}; delete $get_out->{content}->{_embedded}; my $item_id = delete $get_out->{content}->{id}; - if(!$nocheck){ - is_deeply($put_out->{content_in}, $get_out->{content}, "check_put2get: check PUTed item against POSTed item"); + if('CODE' eq ref $check_cb_or_switch){ + $check_cb_or_switch->($put_out,$get_out); + } + if(!$check_cb_or_switch || 'CODE' eq ref $check_cb_or_switch){ + is_deeply($put_out->{content_in}, $get_out->{content}, "check_put2get: check PUTed item against GETed item"); } $get_out->{content}->{id} = $item_id; return ($put_out,$get_out);