Change-Id: I16d00c3c4499c98cc7b9109df2bcefe2d3ae4be4changes/43/5143/5
parent
4438756017
commit
992ba6b46d
@ -0,0 +1,212 @@
|
||||
package NGCP::Panel::Controller::API::CustomerLocations;
|
||||
use NGCP::Panel::Utils::Generic qw(:all);
|
||||
no Moose;
|
||||
use boolean qw(true);
|
||||
use Data::HAL qw();
|
||||
use Data::HAL::Link qw();
|
||||
use HTTP::Headers qw();
|
||||
use HTTP::Status qw(:constants);
|
||||
|
||||
use TryCatch;
|
||||
use NGCP::Panel::Utils::ContractLocations qw();
|
||||
use Path::Tiny qw(path);
|
||||
use Safe::Isa qw($_isa);
|
||||
require Catalyst::ActionRole::ACL;
|
||||
require Catalyst::ActionRole::CheckTrailingSlash;
|
||||
require Catalyst::ActionRole::HTTPMethods;
|
||||
require Catalyst::ActionRole::RequireSSL;
|
||||
|
||||
sub allowed_methods{
|
||||
return [qw/GET POST OPTIONS HEAD/];
|
||||
}
|
||||
|
||||
sub api_description {
|
||||
return 'A Customer Location is a container for a number of network ranges.';
|
||||
};
|
||||
|
||||
sub query_params {
|
||||
return [
|
||||
{
|
||||
param => 'ip',
|
||||
description => 'Filter for customer locations containing a specific IP address',
|
||||
query => {
|
||||
first => \&NGCP::Panel::Utils::ContractLocations::prepare_query_param_value,
|
||||
second => sub {
|
||||
return { join => 'voip_contract_location_blocks',
|
||||
group_by => 'me.id', }
|
||||
#distinct => 1 }; #not neccessary if _CHECK_BLOCK_OVERLAPS was always on
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
param => 'name',
|
||||
description => 'Filter for customer locations matching a name pattern',
|
||||
query => {
|
||||
first => sub {
|
||||
my $q = shift;
|
||||
{ name => { like => $q } };
|
||||
},
|
||||
second => sub {},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
use base qw/Catalyst::Controller NGCP::Panel::Role::API::CustomerLocations/;
|
||||
|
||||
sub resource_name{
|
||||
return 'customerlocations';
|
||||
}
|
||||
sub dispatch_path{
|
||||
return '/api/customerlocations/';
|
||||
}
|
||||
sub relation{
|
||||
return 'http://purl.org/sipwise/ngcp-api/#rel-customerlocations';
|
||||
}
|
||||
|
||||
__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 $cls = $self->item_rs($c);
|
||||
|
||||
(my $total_count, $cls) = $self->paginate_order_collection($c, $cls);
|
||||
my (@embedded, @links);
|
||||
for my $cl ($cls->all) {
|
||||
push @embedded, $self->hal_from_item($c, $cl, $self->resource_name);
|
||||
push @links, Data::HAL::Link->new(
|
||||
relation => 'ngcp:'.$self->resource_name,
|
||||
href => sprintf('%s%d', $self->dispatch_path, $cl->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/');
|
||||
|
||||
push @links, $self->collection_nav_links($page, $rows, $total_count, $c->request->path, $c->request->query_params);
|
||||
|
||||
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 $schema = $c->model('DB');
|
||||
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,
|
||||
exceptions => ['contract_id'],
|
||||
);
|
||||
|
||||
last unless $self->prepare_blocks_resource($c,$resource);
|
||||
my $blocks = delete $resource->{blocks};
|
||||
|
||||
my $cl;
|
||||
try {
|
||||
$cl = $schema->resultset('voip_contract_locations')->create($resource);
|
||||
for my $block (@$blocks) {
|
||||
$cl->create_related('voip_contract_location_blocks', $block);
|
||||
}
|
||||
} catch($e) {
|
||||
$c->log->error("failed to create customer location: $e");
|
||||
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer location.");
|
||||
return;
|
||||
};
|
||||
|
||||
last unless $self->add_create_journal_item_hal($c,sub {
|
||||
my $self = shift;
|
||||
my ($c) = @_;
|
||||
my $_cl = $self->item_by_id($c, $cl->id);
|
||||
return $self->hal_from_item($c, $_cl, $self->resource_name); });
|
||||
|
||||
$guard->commit;
|
||||
|
||||
$c->response->status(HTTP_CREATED);
|
||||
$c->response->header(Location => sprintf('/%s%d', $c->request->path, $cl->id));
|
||||
$c->response->body(q());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub end : Private {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
$self->log_response($c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
no Moose;
|
||||
1;
|
||||
|
||||
# vim: set tabstop=4 expandtab:
|
||||
@ -0,0 +1,232 @@
|
||||
package NGCP::Panel::Controller::API::CustomerLocationsItem;
|
||||
use NGCP::Panel::Utils::Generic qw(:all);
|
||||
no Moose;
|
||||
use boolean qw(true);
|
||||
use Data::HAL qw();
|
||||
use Data::HAL::Link qw();
|
||||
use HTTP::Headers qw();
|
||||
use HTTP::Status qw(:constants);
|
||||
|
||||
use TryCatch;
|
||||
use NGCP::Panel::Utils::ValidateJSON qw();
|
||||
use NGCP::Panel::Utils::Reseller qw();
|
||||
use Path::Tiny qw(path);
|
||||
use Safe::Isa qw($_isa);
|
||||
require Catalyst::ActionRole::ACL;
|
||||
require Catalyst::ActionRole::HTTPMethods;
|
||||
require Catalyst::ActionRole::RequireSSL;
|
||||
|
||||
sub allowed_methods{
|
||||
return [qw/GET OPTIONS HEAD PATCH PUT DELETE/];
|
||||
}
|
||||
|
||||
use base qw/Catalyst::Controller NGCP::Panel::Role::API::CustomerLocations/;
|
||||
|
||||
sub resource_name{
|
||||
return 'customerlocations';
|
||||
}
|
||||
sub dispatch_path{
|
||||
return '/api/customerlocations/';
|
||||
}
|
||||
sub relation{
|
||||
return 'http://purl.org/sipwise/ngcp-api/#rel-customerlocations';
|
||||
}
|
||||
|
||||
sub journal_query_params {
|
||||
my($self,$query_params) = @_;
|
||||
return $self->get_journal_query_params($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 $cl = $self->item_by_id($c, $id);
|
||||
last unless $self->resource_exists($c, customerlocation => $cl);
|
||||
|
||||
my $hal = $self->hal_from_item($c, $cl, $self->resource_name);
|
||||
|
||||
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 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 $cl = $self->item_by_id($c, $id);
|
||||
last unless $self->resource_exists($c, customerlocation => $cl);
|
||||
my $old_resource = $self->hal_from_item($c, $cl, $self->resource_name)->resource;
|
||||
my $resource = $self->apply_patch($c, $old_resource, $json);
|
||||
last unless $resource;
|
||||
|
||||
my $form = $self->get_form($c);
|
||||
$cl = $self->update_item($c, $cl, $old_resource, $resource, $form);
|
||||
last unless $cl;
|
||||
|
||||
my $hal = $self->hal_from_item($c, $cl, $self->resource_name);
|
||||
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, $dset, "destinationsets");
|
||||
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 $cl = $self->item_by_id($c, $id);
|
||||
last unless $self->resource_exists($c, customerlocation => $cl);
|
||||
my $resource = $self->get_valid_put_data(
|
||||
c => $c,
|
||||
id => $id,
|
||||
media_type => 'application/json',
|
||||
);
|
||||
last unless $resource;
|
||||
my $old_resource = { $cl->get_inflated_columns };
|
||||
|
||||
my $form = $self->get_form($c);
|
||||
$cl = $self->update_item($c, $cl, $old_resource, $resource, $form);
|
||||
last unless $cl;
|
||||
|
||||
my $hal = $self->hal_from_item($c, $cl, $self->resource_name);
|
||||
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 $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 $cl = $self->item_by_id($c, $id);
|
||||
last unless $self->resource_exists($c, customerlocation => $cl);
|
||||
try {
|
||||
$cl->delete;
|
||||
} catch($e) {
|
||||
$c->log->error("Failed to delete customre location with id '$id': $e");
|
||||
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error");
|
||||
last;
|
||||
}
|
||||
$guard->commit;
|
||||
|
||||
$c->response->status(HTTP_NO_CONTENT);
|
||||
$c->response->body(q());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
sub get_journal_methods{
|
||||
return [qw/handle_item_base_journal handle_journals_get handle_journalsitem_get handle_journals_options handle_journalsitem_options handle_journals_head handle_journalsitem_head/];
|
||||
}
|
||||
|
||||
sub end : Private {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
$self->log_response($c);
|
||||
return 1;
|
||||
}
|
||||
|
||||
no Moose;
|
||||
1;
|
||||
|
||||
# vim: set tabstop=4 expandtab:
|
||||
@ -0,0 +1,179 @@
|
||||
package NGCP::Panel::Form::Customer::Location;
|
||||
|
||||
use HTML::FormHandler::Moose;
|
||||
extends 'HTML::FormHandler';
|
||||
#use Moose::Util::TypeConstraints;
|
||||
|
||||
use NGCP::Panel::Utils::ContractLocations qw();
|
||||
use Storable qw();
|
||||
|
||||
use HTML::FormHandler::Widget::Block::Bootstrap;
|
||||
|
||||
with 'NGCP::Panel::Render::RepeatableJs';
|
||||
|
||||
has '+widget_wrapper' => ( default => 'Bootstrap' );
|
||||
has_field 'submitid' => ( type => 'Hidden' );
|
||||
sub build_render_list {[qw/submitid fields actions/]}
|
||||
sub build_form_element_class {[qw(form-horizontal)]}
|
||||
|
||||
has_field 'id' => (
|
||||
type => 'Hidden',
|
||||
);
|
||||
|
||||
has_field 'name' => (
|
||||
type => 'Text',
|
||||
label => 'Location Name',
|
||||
required => 1,
|
||||
maxlength => 255,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['Name of the location.']
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'description' => (
|
||||
type => 'Text',
|
||||
label => 'Description',
|
||||
required => 1,
|
||||
maxlength => 255,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['Arbitrary text.'],
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'blocks' => (
|
||||
type => 'Repeatable',
|
||||
required => 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 location blocks, each containing the keys (base) "ip" address and an optional "mask" to specify the network portion (subnet prefix length).'],
|
||||
},
|
||||
validate_method => \&_validate_blocks,
|
||||
inflate_default_method => \&_inflate_blocks,
|
||||
);
|
||||
|
||||
has_field 'blocks.row' => (
|
||||
type => 'Compound',
|
||||
label => 'Location Block',
|
||||
do_label => 1,
|
||||
tags => {
|
||||
before_element => '<div class="ngcp-network-block-row">',
|
||||
after_element => '</div>',
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'blocks.row.ip' => (
|
||||
type => '+NGCP::Panel::Field::IPAddress',
|
||||
required => 1,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['(Base) IP Address']
|
||||
},
|
||||
do_label => 0,
|
||||
do_wrapper => 0,
|
||||
);
|
||||
|
||||
has_field 'blocks.row.mask' => (
|
||||
type => '+NGCP::Panel::Field::PosInteger',
|
||||
required => 0,
|
||||
maxlength => 3,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['Optional Subnet Prefix Length']
|
||||
},
|
||||
do_label => 0,
|
||||
do_wrapper => 0,
|
||||
tags => {
|
||||
before_element => ' /',
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'blocks.rm' => (
|
||||
type => 'RmElement',
|
||||
value => 'Remove',
|
||||
element_class => [qw/btn btn-primary pull-right/],
|
||||
);
|
||||
|
||||
|
||||
has_field 'blocks_add' => (
|
||||
type => 'AddElement',
|
||||
repeatable => 'blocks',
|
||||
value => 'Add another billing network block',
|
||||
element_class => [qw/btn btn-primary pull-right/],
|
||||
);
|
||||
|
||||
|
||||
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/id name description blocks blocks_add/],
|
||||
);
|
||||
|
||||
has_block 'actions' => (
|
||||
tag => 'div',
|
||||
class => [qw/modal-footer/],
|
||||
render_list => [qw/save/],
|
||||
);
|
||||
|
||||
sub _validate_blocks {
|
||||
my ($self,$field) = @_;
|
||||
my $blocks = Storable::dclone($field->value);
|
||||
foreach my $block (@$blocks) {
|
||||
$block->{ip} = $block->{row}->{ip};
|
||||
$block->{mask} = $block->{row}->{mask};
|
||||
delete $block->{row};
|
||||
}
|
||||
NGCP::Panel::Utils::ContractLocations::set_blocks_from_to($blocks,sub {
|
||||
my ($err) = @_;
|
||||
$field->add_error($err);
|
||||
});
|
||||
$field->value($blocks);
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub _inflate_blocks {
|
||||
my ($field,$value) = @_;
|
||||
my @blocks = ();
|
||||
foreach my $block (@$value) {
|
||||
my %row = ();
|
||||
$row{ip} = $block->{ip};
|
||||
$row{mask} = $block->{mask};
|
||||
push(@blocks,{ row => \%row });
|
||||
}
|
||||
return (scalar @blocks == 0 ? undef : \@blocks);
|
||||
}
|
||||
|
||||
sub validate {
|
||||
my ($self) = @_;
|
||||
my $c = $self->ctx;
|
||||
return unless $c;
|
||||
|
||||
my $resource = Storable::dclone($self->values);
|
||||
|
||||
NGCP::Panel::Utils::ContractLocations::check_network_update_item(
|
||||
$c,$resource,$c->stash->{network_result},sub {
|
||||
my ($err,@fields) = @_;
|
||||
foreach my $field (@fields) {
|
||||
$self->field($field)->add_error($err);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
1;
|
||||
# vim: set tabstop=4 expandtab:
|
||||
@ -0,0 +1,62 @@
|
||||
package NGCP::Panel::Form::Customer::LocationAPI;
|
||||
use HTML::FormHandler::Moose;
|
||||
use HTML::FormHandler::Widget::Block::Bootstrap;
|
||||
#use Moose::Util::TypeConstraints;
|
||||
extends 'HTML::FormHandler';
|
||||
|
||||
has_field 'id' => (
|
||||
type => 'Hidden',
|
||||
);
|
||||
|
||||
has_field 'contract_id' => (
|
||||
type => '+NGCP::Panel::Field::PosInteger',
|
||||
required => 1,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['The contract id this location belongs to.']
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'name' => (
|
||||
type => 'Text',
|
||||
required => 1,
|
||||
maxlength => 255,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['The unique name of the location.']
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'description' => (
|
||||
type => 'Text',
|
||||
required => 1,
|
||||
maxlength => 255,
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['Arbitrary text.'],
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'blocks' => (
|
||||
type => 'Repeatable',
|
||||
element_attr => {
|
||||
rel => ['tooltip'],
|
||||
title => ['An array of location blocks, each containing the keys (base) "ip" address and an optional "mask" to specify the network portion (subnet prefix length). The specified blocks must not overlap and can uniformly contain either IPv6 addresses or IPv4 addresses.']
|
||||
},
|
||||
);
|
||||
|
||||
has_field 'blocks.ip' => (
|
||||
type => '+NGCP::Panel::Field::IPAddress',
|
||||
required => 1,
|
||||
label => '(Base) IP Address',
|
||||
);
|
||||
|
||||
has_field 'blocks.mask' => (
|
||||
type => '+NGCP::Panel::Field::PosInteger',
|
||||
required => 0,
|
||||
label => 'Subnet Prefix Length',
|
||||
);
|
||||
|
||||
1;
|
||||
|
||||
# vim: set tabstop=4 expandtab:
|
||||
@ -0,0 +1,142 @@
|
||||
package NGCP::Panel::Role::API::CustomerLocations;
|
||||
use NGCP::Panel::Utils::Generic qw(:all);
|
||||
|
||||
use base 'NGCP::Panel::Role::API';
|
||||
|
||||
|
||||
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::Utils::ContractLocations qw();
|
||||
use NGCP::Panel::Form::Customer::LocationAPI;
|
||||
|
||||
sub get_form {
|
||||
my ($self, $c) = @_;
|
||||
return NGCP::Panel::Form::Customer::LocationAPI->new;
|
||||
}
|
||||
|
||||
sub hal_from_item {
|
||||
my ($self, $c, $item, $type) = @_;
|
||||
my $form;
|
||||
|
||||
my %resource = $item->get_inflated_columns;
|
||||
my @blocks;
|
||||
for my $block ($item->voip_contract_location_blocks->all) {
|
||||
my %blockelem = $block->get_inflated_columns;
|
||||
delete $blockelem{id};
|
||||
delete $blockelem{network_id};
|
||||
delete $blockelem{_ipv4_net_from};
|
||||
delete $blockelem{_ipv4_net_to};
|
||||
delete $blockelem{_ipv6_net_from};
|
||||
delete $blockelem{_ipv6_net_to};
|
||||
push @blocks, \%blockelem;
|
||||
}
|
||||
$resource{blocks} = \@blocks;
|
||||
|
||||
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("%s", $self->dispatch_path)),
|
||||
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:$type", href => sprintf("/api/%s/%d", $type, $item->id)),
|
||||
$self->get_journal_relation_link($item->id),
|
||||
],
|
||||
relation => 'ngcp:'.$self->resource_name,
|
||||
);
|
||||
|
||||
$form //= $self->get_form($c);
|
||||
return unless $self->validate_form(
|
||||
c => $c,
|
||||
form => $form,
|
||||
resource => \%resource,
|
||||
run => 0,
|
||||
exceptions => ['contract_id'],
|
||||
);
|
||||
$hal->resource(\%resource);
|
||||
return $hal;
|
||||
}
|
||||
|
||||
sub _item_rs {
|
||||
my ($self, $c) = @_;
|
||||
|
||||
my $item_rs = $c->model('DB')->resultset('voip_contract_locations');
|
||||
|
||||
if ($c->user->roles eq "reseller") {
|
||||
$item_rs->search({ 'contacts.reseller_id' => $c->user->reseller_id },
|
||||
{ join => { 'contracts' => 'contacts' } });
|
||||
}
|
||||
|
||||
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) = @_;
|
||||
|
||||
delete $resource->{id};
|
||||
my $schema = $c->model('DB');
|
||||
|
||||
$form //= $self->get_form($c);
|
||||
|
||||
return unless $self->validate_form(
|
||||
c => $c,
|
||||
form => $form,
|
||||
resource => $resource,
|
||||
exceptions => ['contract_id'],
|
||||
);
|
||||
|
||||
return unless NGCP::Panel::Utils::ContractLocations::check_network_update_item($c,$resource,$item,sub {
|
||||
my ($err) = @_;
|
||||
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
|
||||
});
|
||||
|
||||
return unless $self->prepare_blocks_resource($c,$resource);
|
||||
my $blocks = delete $resource->{blocks};
|
||||
|
||||
try {
|
||||
$item->update($resource);
|
||||
$item->voip_contract_location_blocks->delete;
|
||||
for my $block (@$blocks) {
|
||||
$item->create_related("voip_contract_location_blocks", $block);
|
||||
}
|
||||
$item->discard_changes;
|
||||
} catch($e) {
|
||||
$self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create customer location.");
|
||||
return;
|
||||
};
|
||||
|
||||
return $item;
|
||||
}
|
||||
|
||||
sub prepare_blocks_resource {
|
||||
my ($self,$c,$resource) = @_;
|
||||
if (! exists $resource->{blocks} ) {
|
||||
$resource->{blocks} = [];
|
||||
}
|
||||
if (ref $resource->{blocks} ne "ARRAY") {
|
||||
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid field 'blocks'. Must be an array.");
|
||||
return 0;
|
||||
}
|
||||
return NGCP::Panel::Utils::ContractLocations::set_blocks_from_to($resource->{blocks},sub {
|
||||
my ($err) = @_;
|
||||
$self->error($c, HTTP_UNPROCESSABLE_ENTITY, $err);
|
||||
});
|
||||
}
|
||||
|
||||
1;
|
||||
# vim: set tabstop=4 expandtab:
|
||||
@ -0,0 +1,167 @@
|
||||
package NGCP::Panel::Utils::ContractLocations;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
#use Sipwise::Base;
|
||||
#use DBIx::Class::Exception;
|
||||
|
||||
use NetAddr::IP;
|
||||
use Data::Validate::IP qw(is_ipv4 is_ipv6);
|
||||
use NGCP::Panel::Utils::IntervalTree::Simple;
|
||||
|
||||
use constant _CHECK_BLOCK_OVERLAPS => 1;
|
||||
|
||||
sub check_network_update_item {
|
||||
my ($c,$new_resource,$old_item,$err_code) = @_;
|
||||
|
||||
return 1 unless $old_item;
|
||||
|
||||
if (!defined $err_code || ref $err_code ne 'CODE') {
|
||||
$err_code = sub { return 0; };
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
}
|
||||
|
||||
sub set_blocks_from_to {
|
||||
my ($blocks,$err_code) = @_;
|
||||
my $intersecter = (_CHECK_BLOCK_OVERLAPS ? NGCP::Panel::Utils::IntervalTree::Simple->new() : undef);
|
||||
my $version;
|
||||
if (!defined $err_code || ref $err_code ne 'CODE') {
|
||||
$err_code = sub { return 0; };
|
||||
}
|
||||
if ((scalar @$blocks) == 0) {
|
||||
return &{$err_code}('At least one block definition is required');
|
||||
}
|
||||
foreach my $block (@$blocks) {
|
||||
if (my $err = _set_ip_net_from_to($block)) {
|
||||
return 0 unless &{$err_code}($err);
|
||||
} else {
|
||||
if ((defined $version && $version == 4 && $block->{_version} != 4) ||
|
||||
(defined $version && $version == 6 && $block->{_version} != 6)) {
|
||||
return &{$err_code}('Ipv4 and ipv6 must not be mixed in block definitions');
|
||||
}
|
||||
$version //= $block->{_version};
|
||||
if (defined $intersecter) {
|
||||
my $to = $block->{_to}->copy->badd(1); #right open intervals
|
||||
my $overlaps_with = $intersecter->find($block->{_from},$to);
|
||||
if ((scalar @$overlaps_with) > 0) {
|
||||
return 0 unless &{$err_code}("Block '$block->{_label}' overlaps with block(s) '" . join("', '",@$overlaps_with) . "'");
|
||||
} else {
|
||||
$intersecter->insert($block->{_from},$to,$block->{_label});
|
||||
}
|
||||
}
|
||||
delete $block->{_from};
|
||||
delete $block->{_to};
|
||||
delete $block->{_version};
|
||||
delete $block->{_label};
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
sub _set_ip_net_from_to {
|
||||
my ($resource) = @_;
|
||||
return "Invalid IP address '$resource->{ip}'" unless _validate_ip($resource->{ip});
|
||||
my $net = NetAddr::IP->new($resource->{ip} . (defined $resource->{mask} ? '/' . $resource->{mask} : ''));
|
||||
if (!defined $net || (defined $resource->{mask} && $net->masklen != $resource->{mask})) {
|
||||
return "Invalid mask '$resource->{mask}'";
|
||||
}
|
||||
$resource->{_label} = $net->cidr;
|
||||
#force scalar context:
|
||||
my $from = $net->network->bigint; #first->bigint;
|
||||
my $to = $net->broadcast->bigint; #last->bigint;
|
||||
#if (NetAddr::IP::Util::hasbits($net->{mask} ^ _CIDR127)) { #other than point-to-point
|
||||
# $from->bsub(1); #include network addr
|
||||
# $to->badd(1); #include broadcast addr
|
||||
#}
|
||||
($resource->{_from},$resource->{_to},$resource->{_version}) = ($from,$to,$net->version);
|
||||
#whatever format we want to save:
|
||||
if ($resource->{_version} == 4) {
|
||||
$resource->{_ipv4_net_from} = $from;
|
||||
$resource->{_ipv4_net_to} = $to;
|
||||
$resource->{_ipv6_net_from} = undef;
|
||||
$resource->{_ipv6_net_to} = undef;
|
||||
} elsif ($resource->{_version} == 6) {
|
||||
$resource->{_ipv4_net_from} = undef;
|
||||
$resource->{_ipv4_net_to} = undef;
|
||||
$resource->{_ipv6_net_from} = $from;
|
||||
$resource->{_ipv6_net_to} = $to;
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
#deflate column values for search parameters doesn't work, so we need this sub
|
||||
#(http://search.cpan.org/~ribasushi/DBIx-Class-0.082820/lib/DBIx/Class/Manual/FAQ.pod#Searching)
|
||||
sub _ip_to_bytes {
|
||||
my ($ip) = @_;
|
||||
if (_validate_ip($ip) && (my $net = NetAddr::IP->new($ip))) {
|
||||
my $bigint = $net->bigint; #force scalar context
|
||||
return (_bigint_to_bytes($bigint,$net->version == 6 ? 16 : 4),$net->version);
|
||||
}
|
||||
return (undef,undef);
|
||||
}
|
||||
|
||||
sub prepare_query_param_value {
|
||||
my ($q) = @_;
|
||||
return _prepare_query_param_value($q);
|
||||
}
|
||||
|
||||
sub _prepare_query_param_value {
|
||||
my ($q,$v) = @_;
|
||||
my ($bytes,$version) = _ip_to_bytes($q);
|
||||
if (defined $v) {
|
||||
return undef if (!defined $bytes || $v != $version);
|
||||
return $bytes;
|
||||
} else {
|
||||
return {} unless defined $bytes;
|
||||
return {
|
||||
'voip_contract_location_blocks._ipv4_net_from' => { '<=', $bytes },
|
||||
'voip_contract_location_blocks._ipv4_net_to' => { '>=', $bytes },
|
||||
} if $version == 4;
|
||||
return {
|
||||
'voip_contract_location_blocks._ipv6_net_from' => { '<=', $bytes },
|
||||
'voip_contract_location_blocks._ipv6_net_to' => { '>=', $bytes },
|
||||
} if $version == 6;
|
||||
}
|
||||
}
|
||||
|
||||
sub _bigint_to_bytes {
|
||||
my ($bigint,$size) = @_;
|
||||
#print '>'.sprintf('%0' . 2 * $size . 's',substr($bigint->as_hex(),2)) . "\n";
|
||||
return pack('C' x $size, map { hex($_) } (sprintf('%0' . 2 * $size . 's',substr($bigint->as_hex(),2)) =~ /(..)/g));
|
||||
#print '>' . join('',map { sprintf('%02x',$_) } unpack('C' x $size, $data)) . "\n";
|
||||
#return $data;
|
||||
}
|
||||
|
||||
sub _validate_ip {
|
||||
my ($ip) = @_;
|
||||
return (is_ipv4($ip) || is_ipv6($ip));
|
||||
}
|
||||
|
||||
sub get_datatable_cols {
|
||||
|
||||
my ($c) = @_;
|
||||
my $grp_stmt = "group_concat(if(`voip_contract_location_blocks`.`mask` is null,`voip_contract_location_blocks`.`ip`,concat(`voip_contract_location_blocks`.`ip`,'/',`voip_contract_location_blocks`.`mask`)) separator ', ')";
|
||||
my $grp_len = 30;
|
||||
return (
|
||||
{ name => 'blocks_grp', accessor => "blocks_grp", search => 0, title => $c->loc('Network Blocks'), literal_sql =>
|
||||
"if(length(".$grp_stmt.") > ".$grp_len.", concat(left(".$grp_stmt.", ".$grp_len."), '...'), ".$grp_stmt.")" },
|
||||
{ name => "voip_contract_location_blocks._ipv4_net_from", search_lower_column => 'ipv4', convert_code => sub {
|
||||
return _prepare_query_param_value(shift,4);
|
||||
} },
|
||||
{ name => "voip_contract_location_blocks._ipv4_net_to", search_upper_column => 'ipv4', convert_code => sub {
|
||||
return _prepare_query_param_value(shift,4);
|
||||
} },
|
||||
{ name => "voip_contract_location_blocks._ipv6_net_from", search_lower_column => 'ipv6', convert_code => sub {
|
||||
return _prepare_query_param_value(shift,6);
|
||||
} },
|
||||
{ name => "voip_contract_location_blocks._ipv6_net_to", search_upper_column => 'ipv6', convert_code => sub {
|
||||
return _prepare_query_param_value(shift,6);
|
||||
} },
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
1;
|
||||
Loading…
Reference in new issue