diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceConfigFiles.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigFiles.pm new file mode 100644 index 0000000000..bc76d7f88e --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigFiles.pm @@ -0,0 +1,80 @@ +package NGCP::Panel::Controller::API::PbxDeviceConfigFiles; +use Sipwise::Base; +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 => + 'Defines the actual PbxDeviceConfigs Files.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + ]}, +); + +with 'NGCP::Panel::Role::API::PbxDeviceFirmwares'; + +class_has('resource_name', is => 'ro', default => 'pbxdeviceconfigfiles'); +class_has('dispatch_path', is => 'ro', default => '/api/pbxdeviceconfigfiles/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-pbxdeviceconfigfiles'); + +__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 OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return; +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceConfigFilesItem.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigFilesItem.pm new file mode 100644 index 0000000000..d77b7e8b5f --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigFilesItem.pm @@ -0,0 +1,85 @@ +package NGCP::Panel::Controller::API::PbxDeviceConfigFilesItem; +use Sipwise::Base; +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::PbxDeviceConfigs'; + +class_has('resource_name', is => 'ro', default => 'pbxdeviceconfigfiles'); +class_has('dispatch_path', is => 'ro', default => '/api/pbxdeviceconfigfiles/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-pbxdeviceconfigfiles'); + +__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, pbxdevicefirmwarebinary => $item); + my $resource = $self->resource_from_item($c, $item); + + $resource->{data} = $item->data; + $c->response->header ('Content-Disposition' => 'attachment; filename="pbxdeviceconfig_' . $item->device_id . '_' . $item->id . '"'); + $c->response->content_type($item->content_type); + $c->response->body($resource->{data}); + 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; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Patch => 'application/json-patch+json', + )); + $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); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceConfigs.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigs.pm index 027b991732..e0b0097d90 100644 --- a/lib/NGCP/Panel/Controller/API/PbxDeviceConfigs.pm +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigs.pm @@ -8,6 +8,8 @@ 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; @@ -18,7 +20,7 @@ class_has 'api_description' => ( is => 'ro', isa => 'Str', default => - 'Specifies a config to be set in PbxDeviceProfiles.', + 'Defines configs for a PbxDeviceModel. To create or update a config, do a POST or PUT with the proper Content-Type (e.g. text/xml) and pass the properties via query parameters, e.g. /api/pbxdeviceconfigs/?device_id=1&version=1.0', ); class_has 'query_params' => ( @@ -26,31 +28,19 @@ class_has 'query_params' => ( isa => 'ArrayRef', default => sub {[ { - param => 'content_type', - description => 'Filter for configs matching a content_type pattern', + param => 'device_id', + description => 'Filter for configs of a specific device model', query => { first => sub { my $q = shift; - { content_type => { like => $q } }; + return { 'device_id' => $q }; }, - second => sub {}, - }, - }, - { - param => 'version', - description => 'Filter for configs matching a version name pattern', - query => { - first => sub { - my $q = shift; - { version => { like => $q } }; - }, - second => sub {}, + second => sub { }, }, }, ]}, ); - with 'NGCP::Panel::Role::API::PbxDeviceConfigs'; class_has('resource_name', is => 'ro', default => 'pbxdeviceconfigs'); @@ -66,7 +56,7 @@ __PACKAGE__->config( Does => [qw(ACL CheckTrailingSlash RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - } } @{ __PACKAGE__->allowed_methods }, + } } @{ __PACKAGE__->allowed_methods } }, action_roles => [qw(HTTPMethods)], ); @@ -76,7 +66,6 @@ sub auto :Private { $self->set_body($c); $self->log_request($c); - return 1; } sub GET :Allow { @@ -84,15 +73,15 @@ sub GET :Allow { my $page = $c->request->params->{page} // 1; my $rows = $c->request->params->{rows} // 10; { - my $field_devs = $self->item_rs($c); - - (my $total_count, $field_devs) = $self->paginate_order_collection($c, $field_devs); + my $items = $self->item_rs($c); + (my $total_count, $items) = $self->paginate_order_collection($c, $items); my (@embedded, @links); - for my $dev ($field_devs->all) { - push @embedded, $self->hal_from_item($c, $dev); + 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', $self->dispatch_path, $dev->id), + href => sprintf('/%s%d', $c->request->path, $item->id), ); } push @links, @@ -103,12 +92,12 @@ sub GET :Allow { 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)); + 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', $self->dispatch_path, $page + 1, $rows)); + 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', $self->dispatch_path, $page - 1, $rows)); + 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( @@ -146,11 +135,67 @@ sub OPTIONS :Allow { return; } +sub POST :Allow { + my ($self, $c) = @_; + + my $guard = $c->model('DB')->txn_scope_guard; + { + my $data = $self->get_valid_raw_post_data( + c => $c, + media_type => [qw#text/plain text/xml#], + ); + last unless $data; + my $resource = $c->req->query_params; + + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + exceptions => [ "device_id" ], + ); + + my $model_rs = $c->model('DB')->resultset('autoprov_devices')->search({ + id => $resource->{device_id} + }); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $model_rs = $model_rs->search({ + reseller_id => $c->user->reseller_id, + }); + } + my $model = $model_rs->first; + unless($model) { + $c->log->error("invalid device_id '$$resource{device_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device model does not exist"); + last; + } + + $resource->{data} = $data; + $resource->{content_type} = $c->request->header('Content-Type'); + + my $item; + try { + $item = $model->autoprov_configs->create($resource); + } catch($e) { + $c->log->error("failed to create pbxdeviceconfig: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create pbxdeviceconfig."); + 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); - return 1; } # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceConfigsItem.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigsItem.pm index aac22fec25..a7a1d8cdbf 100644 --- a/lib/NGCP/Panel/Controller/API/PbxDeviceConfigsItem.pm +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceConfigsItem.pm @@ -1,14 +1,11 @@ package NGCP::Panel::Controller::API::PbxDeviceConfigsItem; use Sipwise::Base; 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 NGCP::Panel::Utils::ValidateJSON qw(); use Path::Tiny qw(path); use Safe::Isa qw($_isa); BEGIN { extends 'Catalyst::Controller::ActionRole'; } @@ -31,7 +28,7 @@ __PACKAGE__->config( Does => [qw(ACL RequireSSL)], Method => $_, Path => __PACKAGE__->dispatch_path, - } } @{ __PACKAGE__->allowed_methods }, + } } @{ __PACKAGE__->allowed_methods } }, action_roles => [qw(HTTPMethods)], ); @@ -41,22 +38,22 @@ sub auto :Private { $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 $field_dev = $self->item_by_id($c, $id); - last unless $self->resource_exists($c, deviceprofile => $field_dev); + my $item = $self->item_by_id($c, $id); + last unless $self->resource_exists($c, pbxdeviceconfig => $item); - my $hal = $self->hal_from_item($c, $field_dev); + my $hal = $self->hal_from_item($c, $item); my $response = HTTP::Response->new(HTTP_OK, undef, HTTP::Headers->new( (map { # XXX Data::HAL must be able to generate links with multiple relations - s|rel="(http://purl.org/sipwise/ngcp-api/#rel-resellers)"|rel="item $1"|r - =~ s/rel=self/rel="item self"/r; + 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); @@ -85,11 +82,71 @@ sub OPTIONS :Allow { 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, pbxdeviceconfig => $item); + my $data = $self->get_valid_raw_put_data( + c => $c, + id => $id, + media_type => [qw#text/plain text/xml#], + ); + last unless $data; + my $resource = $c->req->query_params; + $resource->{data} = $data; + my $form = $self->get_form($c); + my $old_resource = $self->resource_from_item($c, $item, $form); + + $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; +} + +=pod +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, pbxdeviceconfig => $item); + $item->delete; + + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} +=cut + sub end : Private { my ($self, $c) = @_; $self->log_response($c); - return 1; } # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwareBinaries.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwareBinaries.pm new file mode 100644 index 0000000000..e4513377e9 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwareBinaries.pm @@ -0,0 +1,80 @@ +package NGCP::Panel::Controller::API::PbxDeviceFirmwareBinaries; +use Sipwise::Base; +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 => + 'Defines the actual PbxDeviceFirmwares Binary.', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + ]}, +); + +with 'NGCP::Panel::Role::API::PbxDeviceFirmwares'; + +class_has('resource_name', is => 'ro', default => 'pbxdevicefirmwarebinaries'); +class_has('dispatch_path', is => 'ro', default => '/api/pbxdevicefirmwarebinaries/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-pbxdevicefirmwarebinaries'); + +__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 OPTIONS :Allow { + my ($self, $c) = @_; + my $allowed_methods = $self->allowed_methods; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Post => 'application/hal+json; profile=http://purl.org/sipwise/ngcp-api/#rel-'.$self->resource_name, + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); + return; +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwareBinariesItem.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwareBinariesItem.pm new file mode 100644 index 0000000000..5f4ee05811 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwareBinariesItem.pm @@ -0,0 +1,85 @@ +package NGCP::Panel::Controller::API::PbxDeviceFirmwareBinariesItem; +use Sipwise::Base; +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::PbxDeviceFirmwares'; + +class_has('resource_name', is => 'ro', default => 'pbxdevicefirmwarebinaries'); +class_has('dispatch_path', is => 'ro', default => '/api/pbxdevicefirmwarebinaries/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-pbxdevicefirmwarebinaries'); + +__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, pbxdevicefirmwarebinary => $item); + my $resource = $self->resource_from_item($c, $item); + + $resource->{data} = $item->data; + $c->response->header ('Content-Disposition' => 'attachment; filename="' . $resource->{filename} . '"'); + $c->response->content_type('application/octet-stream'); + $c->response->body($resource->{data}); + 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; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Patch => 'application/json-patch+json', + )); + $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); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwares.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwares.pm new file mode 100644 index 0000000000..5a7ea82ba4 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwares.pm @@ -0,0 +1,201 @@ +package NGCP::Panel::Controller::API::PbxDeviceFirmwares; +use Sipwise::Base; +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 => + 'Defines firmwares for a PbxDeviceModel. To create or update a firmware, do a POST or PUT with Content-Type application/octet-stream and pass the properties via query parameters, e.g. /api/pbxdevicefirmwares/?device_id=1&filename=test.bin&version=1.0', +); + +class_has 'query_params' => ( + is => 'ro', + isa => 'ArrayRef', + default => sub {[ + { + param => 'device_id', + description => 'Filter for firmwares of a specific device model', + query => { + first => sub { + my $q = shift; + return { 'device_id' => $q }; + }, + second => sub { }, + }, + }, + ]}, +); + +with 'NGCP::Panel::Role::API::PbxDeviceFirmwares'; + +class_has('resource_name', is => 'ro', default => 'pbxdevicefirmwares'); +class_has('dispatch_path', is => 'ro', default => '/api/pbxdevicefirmwares/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-pbxdevicefirmwares'); + +__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; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + 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 $binary = $self->get_valid_raw_post_data( + c => $c, + media_type => 'application/octet-stream', + ); + last unless $binary; + my $resource = $c->req->query_params; + + my $form = $self->get_form($c); + last unless $self->validate_form( + c => $c, + resource => $resource, + form => $form, + exceptions => [ "device_id" ], + ); + + my $model_rs = $c->model('DB')->resultset('autoprov_devices')->search({ + id => $resource->{device_id} + }); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $model_rs = $model_rs->search({ + reseller_id => $c->user->reseller_id, + }); + } + my $model = $model_rs->first; + unless($model) { + $c->log->error("invalid device_id '$$resource{device_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device model does not exist"); + last; + } + + $resource->{data} = $binary; + last unless($resource); + + my $item; + try { + $item = $model->autoprov_firmwares->create($resource); + } catch($e) { + $c->log->error("failed to create pbxdevicefirmware: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create pbxdevicefirmware."); + 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); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwaresItem.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwaresItem.pm new file mode 100644 index 0000000000..9887e66519 --- /dev/null +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceFirmwaresItem.pm @@ -0,0 +1,152 @@ +package NGCP::Panel::Controller::API::PbxDeviceFirmwaresItem; +use Sipwise::Base; +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::PbxDeviceFirmwares'; + +class_has('resource_name', is => 'ro', default => 'pbxdevicefirmwares'); +class_has('dispatch_path', is => 'ro', default => '/api/pbxdevicefirmwares/'); +class_has('relation', is => 'ro', default => 'http://purl.org/sipwise/ngcp-api/#rel-pbxdevicefirmwares'); + +__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, pbxdevicefirmware => $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; + $c->response->headers(HTTP::Headers->new( + Allow => $allowed_methods->join(', '), + Accept_Patch => 'application/json-patch+json', + )); + $c->response->content_type('application/json'); + $c->response->body(JSON::to_json({ methods => $allowed_methods })."\n"); + return; +} + +sub PUT :Allow { + my ($self, $c, $id) = @_; + my $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, pbxdevicefirmware => $item); + my $recording = $self->get_valid_raw_put_data( + c => $c, + id => $id, + media_type => 'application/octet-stream', + ); + last unless $recording; + my $resource = $c->req->query_params; + $resource->{data} = $recording; + my $form = $self->get_form($c); + my $old_resource = $self->resource_from_item($c, $item, $form); + + $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; +} + +=pod +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, pbxdevicefirmware => $item); + $item->delete; + + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} +=cut + +sub end : Private { + my ($self, $c) = @_; + + $self->log_response($c); +} + +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceModels.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceModels.pm index f357cfac92..f7b1b6e1d7 100644 --- a/lib/NGCP/Panel/Controller/API/PbxDeviceModels.pm +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceModels.pm @@ -25,6 +25,17 @@ class_has 'query_params' => ( is => 'ro', isa => 'ArrayRef', default => sub {[ + { + param => 'reseller_id', + description => 'Filter for models belonging to a certain reseller', + query => { + first => sub { + my $q = shift; + { reseller_id => $q }; + }, + second => sub {}, + }, + }, { param => 'model', description => 'Filter for models matching a model name pattern', @@ -146,6 +157,82 @@ sub OPTIONS :Allow { 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, + ); + + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $resource->{reseller_id} = $c->user->reseller_id; + } + + my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id}); + unless($reseller) { + $c->log->error("invalid reseller_id '$$resource{reseller_id}', does not exist"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid reseller_id, does not exist"); + last; + } + + my $item; + $item = $c->model('DB')->resultset('autoprov_devices')->find({ + reseller_id => $resource->{reseller_id}, + vendor => $resource->{vendor}, + model => $resource->{model}, + }); + if($item) { + $c->log->error("device model with vendor '$$resource{vendor}' and model '$$resource{model}'already exists for reseller_id '$$resource{reseller_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Device model already exists for this reseller"); + last; + } + + my $linerange = delete $resource->{linerange}; + unless(ref $linerange eq "ARRAY") { + $c->log->error("linerange must be array"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid linerange parameter, must be array"); + last; + } + + + try { + $item = $c->model('DB')->resultset('autoprov_devices')->create($resource); + foreach my $range(@{ $linerange }) { + unless(ref $range eq "HASH") { + use Data::Dumper; + $c->log->error("all elements in linerange must be hashes, but this is " . ref $range . ": " . Dumper $range); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid range definition inside linerange parameter, all must be hash"); + return; + } + $item->autoprov_device_line_ranges->create($range); + } + } catch($e) { + $c->log->error("failed to create device model: $e"); + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create device model."); + 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) = @_; diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceModelsItem.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceModelsItem.pm index ba06797dae..ca5983ec18 100644 --- a/lib/NGCP/Panel/Controller/API/PbxDeviceModelsItem.pm +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceModelsItem.pm @@ -85,6 +85,116 @@ sub OPTIONS :Allow { 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, pbxdevicemodel => $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, pbxdevicemodel => $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; +} + +=pod +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, emailtemplate => $item); + foreach(qw/subscriber_email_template_id passreset_email_template_id invoice_email_template_id/){ + $c->model('DB')->resultset('contracts')->search({ + $_ => $item->id, + })->update({ + $_ => undef, + }); + } + + $item->delete; + + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} +=cut + sub end : Private { my ($self, $c) = @_; diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceProfiles.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceProfiles.pm index a28a87cb25..a8806f54b0 100644 --- a/lib/NGCP/Panel/Controller/API/PbxDeviceProfiles.pm +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceProfiles.pm @@ -135,6 +135,70 @@ sub OPTIONS :Allow { 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, + ); + + my $item; + $item = $c->model('DB')->resultset('autoprov_profiles')->find({ + config_id => $resource->{config_id}, + name => $resource->{name}, + }); + if($item) { + $c->log->error("Pbx device profile with name '$$resource{name}' already exists for config_id '$$resource{config_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device profile with this name already exists for this config"); + last; + } + my $config_rs = $c->model('DB')->resultset('autoprov_configs')->search({ + id => $resource->{config_id}, + }); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $config_rs = $config_rs->search({ + 'device.reseller_id' => $c->user->reseller_id, + },{ + join => 'device', + }); + } + my $config = $config_rs->first; + unless($config) { + $c->log->error("Pbx device config with confg_id '$$resource{config_id}' does not exist"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device config does not exist"); + last; + } + + try { + $item = $config->autoprov_profiles->create($resource); + } catch($e) { + $c->log->error("failed to create pbx device profile: $e"); # TODO: user, message, trace, ... + $self->error($c, HTTP_INTERNAL_SERVER_ERROR, "Failed to create pbx device profile."); + 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) = @_; diff --git a/lib/NGCP/Panel/Controller/API/PbxDeviceProfilesItem.pm b/lib/NGCP/Panel/Controller/API/PbxDeviceProfilesItem.pm index 00fa76ca4a..ba32b1268f 100644 --- a/lib/NGCP/Panel/Controller/API/PbxDeviceProfilesItem.pm +++ b/lib/NGCP/Panel/Controller/API/PbxDeviceProfilesItem.pm @@ -85,6 +85,116 @@ sub OPTIONS :Allow { 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, pbxdeviceprofile => $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, pbxdeviceprofile => $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; +} + +=pod +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, emailtemplate => $item); + foreach(qw/subscriber_email_template_id passreset_email_template_id invoice_email_template_id/){ + $c->model('DB')->resultset('contracts')->search({ + $_ => $item->id, + })->update({ + $_ => undef, + }); + } + + $item->delete; + + $guard->commit; + + $c->response->status(HTTP_NO_CONTENT); + $c->response->body(q()); + } + return; +} +=cut + sub end : Private { my ($self, $c) = @_; diff --git a/lib/NGCP/Panel/Form/Device/ConfigAPI.pm b/lib/NGCP/Panel/Form/Device/ConfigAPI.pm new file mode 100644 index 0000000000..9bf99e86ff --- /dev/null +++ b/lib/NGCP/Panel/Form/Device/ConfigAPI.pm @@ -0,0 +1,51 @@ +package NGCP::Panel::Form::Device::ConfigAPI; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; +use Moose::Util::TypeConstraints; + +use HTML::FormHandler::Widget::Block::Bootstrap; + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +has_field 'submitid' => ( type => 'Hidden' ); +has '+enctype' => ( default => 'multipart/form-data'); +sub build_render_list {[qw/submitid fields actions/]} +sub build_form_element_class {[qw(form-horizontal)]} + +has_field 'device_id' => ( + type => 'PosInteger', + required => 1, + label => 'Device Model', + element_attr => { + rel => ['tooltip'], + title => ['The pbx device model id this config belongs to.'] + }, +); + +has_field 'version' => ( + type => 'Text', + required => 1, + label => 'Version', + element_attr => { + rel => ['tooltip'], + title => ['The version number of this config.'] + }, +); + +has_field 'content_type' => ( + type => 'Text', + label => 'Filename', + element_attr => { + rel => ['tooltip'], + title => ['The content type this config is served as.'] + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/device_id version content_type/], +); + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Device/FirmwareAPI.pm b/lib/NGCP/Panel/Form/Device/FirmwareAPI.pm new file mode 100644 index 0000000000..b5b0bcbbae --- /dev/null +++ b/lib/NGCP/Panel/Form/Device/FirmwareAPI.pm @@ -0,0 +1,52 @@ +package NGCP::Panel::Form::Device::FirmwareAPI; + +use HTML::FormHandler::Moose; +extends 'HTML::FormHandler'; +use Moose::Util::TypeConstraints; + +use HTML::FormHandler::Widget::Block::Bootstrap; + +has '+widget_wrapper' => ( default => 'Bootstrap' ); +has_field 'submitid' => ( type => 'Hidden' ); +has '+enctype' => ( default => 'multipart/form-data'); +sub build_render_list {[qw/submitid fields actions/]} +sub build_form_element_class {[qw(form-horizontal)]} + +has_field 'device_id' => ( + type => 'PosInteger', + required => 1, + label => 'Device Model', + element_attr => { + rel => ['tooltip'], + title => ['The pbx device model id this firmware belongs to.'] + }, +); + +has_field 'version' => ( + type => 'Text', + required => 1, + label => 'Version', + element_attr => { + rel => ['tooltip'], + title => ['The version number of this firmware.'] + }, +); + +has_field 'filename' => ( + type => 'Text', + required => 1, + label => 'Filename', + element_attr => { + rel => ['tooltip'], + title => ['The filename of this firmware.'] + }, +); + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/device_id version filename/], +); + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Form/Device/Model.pm b/lib/NGCP/Panel/Form/Device/Model.pm index 2d178b0206..0ad37c8c54 100644 --- a/lib/NGCP/Panel/Form/Device/Model.pm +++ b/lib/NGCP/Panel/Form/Device/Model.pm @@ -18,12 +18,20 @@ has_field 'vendor' => ( type => 'Text', required => 1, label => 'Vendor', + element_attr => { + rel => ['tooltip'], + title => ['The vendor name of this device.'], + }, ); has_field 'model' => ( type => 'Text', required => 1, label => 'Model', + element_attr => { + rel => ['tooltip'], + title => ['The model name of this device.'], + }, ); has_field 'front_image' => ( @@ -45,6 +53,10 @@ has_field 'sync_uri' => ( required => 0, label => 'Bootstrap Sync URI', default => 'http://[% client.ip %]/admin/resync', + element_attr => { + rel => ['tooltip'], + title => ['The sync URI to set the provisioning server of the device (e.g. http://client.ip/admin/resync. The client.ip variable is automatically expanded during provisioning time.'], + }, ); has_field 'sync_method' => ( @@ -56,6 +68,10 @@ has_field 'sync_method' => ( { label => 'POST', value => 'POST' }, ], default => 'GET', + element_attr => { + rel => ['tooltip'], + title => ['The HTTP method to set the provisioning server (one of GET, POST).'], + }, ); has_field 'sync_params' => ( @@ -63,6 +79,10 @@ has_field 'sync_params' => ( required => 0, label => 'Bootstrap Sync Parameters', default => '[% server.uri %]/$MA', + element_attr => { + rel => ['tooltip'], + title => ['The parameters appended to the sync URI when setting the provisioning server, e.g. server.uri/$MA. The server.uri variable is automatically expanded during provisioning time.'], + }, ); has_field 'linerange' => ( @@ -76,6 +96,10 @@ has_field 'linerange' => ( controls_div => 1, }, wrapper_class => [qw/hfh-rep-block/], + element_attr => { + rel => ['tooltip'], + title => ['An array of line/key definitions for this device. Each element is a hash containing the keys name, num_lines (defining the number of lines/keys for this range), can_private, can_shared, can_blf.'], + }, ); has_field 'linerange.id' => ( diff --git a/lib/NGCP/Panel/Form/Device/ModelAPI.pm b/lib/NGCP/Panel/Form/Device/ModelAPI.pm new file mode 100644 index 0000000000..dce3d7b365 --- /dev/null +++ b/lib/NGCP/Panel/Form/Device/ModelAPI.pm @@ -0,0 +1,25 @@ +package NGCP::Panel::Form::Device::ModelAPI; + +use HTML::FormHandler::Moose; +extends 'NGCP::Panel::Form::Device::ModelAdmin'; +use Moose::Util::TypeConstraints; + +has_block 'fields' => ( + tag => 'div', + class => [qw/modal-body/], + render_list => [qw/reseller vendor model linerange sync_uri sync_method sync_params/], +); + +override 'field_list' => sub { + my $self = shift; + my $c = $self->ctx; + return unless $c; + + super(); + foreach my $f(qw/front_image mac_image linerange_add/) { + $self->field($f)->inactive(1); + } +}; + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API.pm b/lib/NGCP/Panel/Role/API.pm index 992d5843a7..d324028251 100644 --- a/lib/NGCP/Panel/Role/API.pm +++ b/lib/NGCP/Panel/Role/API.pm @@ -200,9 +200,18 @@ sub forbid_link_header { sub valid_media_type { my ($self, $c, $media_type) = @_; - return 1 if($c->request->header('Content-Type') && + + my $type; + if(ref $media_type eq "ARRAY") { + $type = join ' or ', @{ $media_type }; + return 1 if $c->request->header('Content-Type') && + $c->request->header('Content-Type') ~~ $media_type; + } else { + $type = $media_type; + return 1 if($c->request->header('Content-Type') && index($c->request->header('Content-Type'), $media_type) == 0); - $self->error($c, HTTP_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type, accepting '$media_type' only."); + } + $self->error($c, HTTP_UNSUPPORTED_MEDIA_TYPE, "Unsupported media type, accepting $type only."); return; } diff --git a/lib/NGCP/Panel/Role/API/PbxDeviceConfigs.pm b/lib/NGCP/Panel/Role/API/PbxDeviceConfigs.pm index ed54338027..dc476a6484 100644 --- a/lib/NGCP/Panel/Role/API/PbxDeviceConfigs.pm +++ b/lib/NGCP/Panel/Role/API/PbxDeviceConfigs.pm @@ -11,18 +11,33 @@ use TryCatch; use Data::HAL qw(); use Data::HAL::Link qw(); use HTTP::Status qw(:constants); -use JSON::Types; -use NGCP::Panel::Form::Device::Config; +use NGCP::Panel::Form::Device::ConfigAPI; + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs = $c->model('DB')->resultset('autoprov_configs'); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ + 'device.reseller_id' => $c->user->reseller_id + },{ + join => 'device', + }); + } + return $item_rs; +} sub get_form { my ($self, $c) = @_; - return NGCP::Panel::Form::Device::Config->new; + return NGCP::Panel::Form::Device::ConfigAPI->new(ctx => $c); } sub hal_from_item { - my ($self, $c, $item) = @_; - my $form; - my $type = 'pbxdeviceconfigs'; + my ($self, $c, $item, $form) = @_; + + $form //= $self->get_form($c); + my $resource = $self->resource_from_item($c, $item, $form); my $hal = Data::HAL->new( links => [ @@ -32,56 +47,80 @@ sub hal_from_item { name => 'ngcp', templated => true, ), - Data::HAL::Link->new(relation => 'collection', href => sprintf("%s", $self->dispatch_path)), + 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:$type", href => sprintf("/api/%s/%d", $type, $item->id)), Data::HAL::Link->new(relation => 'ngcp:pbxdevicemodels', href => sprintf("/api/pbxdevicemodels/%d", $item->device_id)), + Data::HAL::Link->new(relation => 'ngcp:pbxdeviceconfigfiles', href => sprintf("/api/pbxdeviceconfigfiles/%d", $item->id)), ], relation => 'ngcp:'.$self->resource_name, ); - my $resource = $self->resource_from_item($c, $item); + + $self->validate_form( + c => $c, + resource => $resource, + form => $form, + run => 0, + exceptions => [qw/device_id/], + ); + + $resource->{id} = int($item->id); + delete $resource->{data}; $hal->resource($resource); return $hal; } sub resource_from_item { - my ($self, $c, $item) = @_; + my ($self, $c, $item, $form) = @_; + + my $resource = { $item->get_inflated_columns }; + delete $resource->{data}; - my %resource = $item->get_inflated_columns; - delete $resource{device_id}; + return $resource; +} - my $form = $self->get_form($c); +sub item_by_id { + my ($self, $c, $id) = @_; + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + my $binary = delete $resource->{data}; + $resource->{content_type} = $c->request->header('Content-Type'); + + $form //= $self->get_form($c); return unless $self->validate_form( c => $c, form => $form, - resource => \%resource, - run => 0, + resource => $resource, + exceptions => [qw/device_id/], ); - $resource{id} = int($item->id); - return \%resource; -} - -sub item_rs { - my ($self, $c) = @_; - my $item_rs = $c->model('DB')->resultset('autoprov_configs'); + my $model_rs = $c->model('DB')->resultset('autoprov_devices')->search({ + id => $resource->{device_id} + }); if($c->user->roles eq "admin") { - } elsif ($c->user->roles eq "reseller") { - $item_rs = $item_rs->search( - { 'device.reseller_id' => $c->user->reseller_id, }, - { prefetch => 'device', }); + } elsif($c->user->roles eq "reseller") { + $model_rs = $model_rs->search({ + reseller_id => $c->user->reseller_id, + }); + } + my $model = $model_rs->first; + unless($model) { + $c->log->error("invalid device_id '$$resource{device_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device model does not exist"); + last; } - return $item_rs; -} + $resource->{data} = $binary; -sub item_by_id { - my ($self, $c, $id) = @_; + $item->update($resource); - my $item_rs = $self->item_rs($c); - return $item_rs->find($id); + return $item; } 1; diff --git a/lib/NGCP/Panel/Role/API/PbxDeviceFirmwares.pm b/lib/NGCP/Panel/Role/API/PbxDeviceFirmwares.pm new file mode 100644 index 0000000000..cf3acbcd65 --- /dev/null +++ b/lib/NGCP/Panel/Role/API/PbxDeviceFirmwares.pm @@ -0,0 +1,125 @@ +package NGCP::Panel::Role::API::PbxDeviceFirmwares; +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::Device::FirmwareAPI; + +sub item_rs { + my ($self, $c) = @_; + + my $item_rs = $c->model('DB')->resultset('autoprov_firmwares'); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $item_rs = $item_rs->search({ + 'device.reseller_id' => $c->user->reseller_id + },{ + join => 'device', + }); + } + return $item_rs; +} + +sub get_form { + my ($self, $c) = @_; + return NGCP::Panel::Form::Device::FirmwareAPI->new(ctx => $c); +} + +sub hal_from_item { + my ($self, $c, $item, $form) = @_; + + $form //= $self->get_form($c); + my $resource = $self->resource_from_item($c, $item, $form); + + 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:pbxdevicemodels', href => sprintf("/api/pbxdevicemodels/%d", $item->device_id)), + Data::HAL::Link->new(relation => 'ngcp:pbxdevicefirmwarebinaries', href => sprintf("/api/pbxdevicefirmwarebinaries/%d", $item->id)), + ], + relation => 'ngcp:'.$self->resource_name, + ); + + + $self->validate_form( + c => $c, + resource => $resource, + form => $form, + run => 0, + exceptions => [qw/device_id/], + ); + + $resource->{id} = int($item->id); + delete $resource->{data}; + $hal->resource($resource); + return $hal; +} + +sub resource_from_item { + my ($self, $c, $item, $form) = @_; + + my $resource = { $item->get_inflated_columns }; + delete $resource->{data}; + + return $resource; +} + +sub item_by_id { + my ($self, $c, $id) = @_; + my $item_rs = $self->item_rs($c); + return $item_rs->find($id); +} + +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + my $binary = delete $resource->{data}; + $form //= $self->get_form($c); + return unless $self->validate_form( + c => $c, + form => $form, + resource => $resource, + exceptions => [qw/device_id/], + ); + + my $model_rs = $c->model('DB')->resultset('autoprov_devices')->search({ + id => $resource->{device_id} + }); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $model_rs = $model_rs->search({ + reseller_id => $c->user->reseller_id, + }); + } + my $model = $model_rs->first; + unless($model) { + $c->log->error("invalid device_id '$$resource{device_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device model does not exist"); + last; + } + + $resource->{data} = $binary; + + $item->update($resource); + + return $item; +} + +1; +# vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/PbxDeviceModels.pm b/lib/NGCP/Panel/Role/API/PbxDeviceModels.pm index c9bcb83046..8e59657058 100644 --- a/lib/NGCP/Panel/Role/API/PbxDeviceModels.pm +++ b/lib/NGCP/Panel/Role/API/PbxDeviceModels.pm @@ -12,17 +12,17 @@ use Data::HAL qw(); use Data::HAL::Link qw(); use HTTP::Status qw(:constants); use JSON::Types; -use NGCP::Panel::Form::Device::Model; +use NGCP::Panel::Form::Device::ModelAPI; sub get_form { my ($self, $c) = @_; - return NGCP::Panel::Form::Device::Model->new; + return NGCP::Panel::Form::Device::ModelAPI->new($c); } sub hal_from_item { my ($self, $c, $item) = @_; my $form; - my $type = 'pbxdevicemodels'; + #my $type = 'pbxdevicemodels'; my $hal = Data::HAL->new( links => [ @@ -35,7 +35,7 @@ sub hal_from_item { 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)), + Data::HAL::Link->new(relation => "ngcp:pbxdevicefirmwares", href => sprintf("/api/pbxdevicefirmwares/?device_id=%d", $item->id)), ], relation => 'ngcp:'.$self->resource_name, ); @@ -62,6 +62,19 @@ sub resource_from_item { $resource{reseller_id} = int($item->reseller_id); $resource{id} = int($item->id); + $resource{linerange} = []; + foreach my $range($item->autoprov_device_line_ranges->all) { + my $r = { $range->get_inflated_columns }; + foreach my $f(qw/device_id/) { + delete $r->{$f}; + } + $r->{id} = int($r->{id}); + foreach my $f(qw/can_private can_shared can_blf/) { + $r->{$f} = JSON::Types::bool($r->{$f}); + } + + push @{ $resource{linerange} }, $r; + } return \%resource; } @@ -83,5 +96,88 @@ sub item_by_id { return $item_rs->find($id); } +sub update_item { + + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + $form //= $self->get_form($c); + + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $resource->{reseller_id} = $c->user->reseller_id; + } + + my $reseller = $c->model('DB')->resultset('resellers')->find($resource->{reseller_id}); + unless($reseller) { + $c->log->error("invalid reseller_id '$$resource{reseller_id}', does not exist"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid reseller_id, does not exist"); + return; + } + + my $dup_item; + $dup_item = $c->model('DB')->resultset('autoprov_devices')->find({ + reseller_id => $resource->{reseller_id}, + vendor => $resource->{vendor}, + model => $resource->{model}, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("device model with vendor '$$resource{vendor}' and model '$$resource{model}'already exists for reseller_id '$$resource{reseller_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Device model already exists for this reseller"); + return; + } + + my $linerange = delete $resource->{linerange}; + unless(ref $linerange eq "ARRAY") { + $c->log->error("linerange must be array"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Invalid linerange parameter, must be array"); + return; + } + + $item->update($resource); + + my @existing_range = (); + my $range_rs = $item->autoprov_device_line_ranges; + foreach my $range(@{ $linerange }) { + next unless(defined $range); + my $old_range; + if(defined $range->{id}) { + # should be an existing range, do update + $old_range = $range_rs->find($range->{id}); + delete $range->{id}; + unless($old_range) { + $old_range = $range_rs->create($range); + } else { + # formhandler only passes set check-boxes, so explicitely unset here + $range->{can_private} //= 0; + $range->{can_shared} //= 0; + $range->{can_blf} //= 0; + $old_range->update($range); + } + } else { + # new range + $old_range = $range_rs->create($range); + } + push @existing_range, $old_range->id; # mark as valid (delete others later) + + # delete field device line assignments with are out-of-range or use a + # feature which is not supported anymore after edit + foreach my $fielddev_line($c->model('DB')->resultset('autoprov_field_device_lines') + ->search({ linerange_id => $old_range->id })->all) { + if($fielddev_line->key_num >= $old_range->num_lines || + ($fielddev_line->line_type eq 'private' && !$old_range->can_private) || + ($fielddev_line->line_type eq 'shared' && !$old_range->can_shared) || + ($fielddev_line->line_type eq 'blf' && !$old_range->can_blf)) { + + $fielddev_line->delete; + } + } + } + # delete invalid range ids (e.g. removed ones) + $range_rs->search({ + id => { 'not in' => \@existing_range }, + })->delete_all; + + return $item; +} + 1; # vim: set tabstop=4 expandtab: diff --git a/lib/NGCP/Panel/Role/API/PbxDeviceProfiles.pm b/lib/NGCP/Panel/Role/API/PbxDeviceProfiles.pm index ea930df0c3..91359bbdda 100644 --- a/lib/NGCP/Panel/Role/API/PbxDeviceProfiles.pm +++ b/lib/NGCP/Panel/Role/API/PbxDeviceProfiles.pm @@ -61,6 +61,7 @@ sub resource_from_item { ); $resource{id} = int($item->id); + $resource{config_id} = int($item->config_id); return \%resource; } @@ -84,5 +85,41 @@ sub item_by_id { return $item_rs->find($id); } +sub update_item { + my ($self, $c, $item, $old_resource, $resource, $form) = @_; + + my $dup_item = $c->model('DB')->resultset('autoprov_profiles')->find({ + config_id => $resource->{config_id}, + name => $resource->{name}, + }); + if($dup_item && $dup_item->id != $item->id) { + $c->log->error("Pbx device profile with name '$$resource{name}' already exists for config_id '$$resource{config_id}'"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device profile with this name already exists for this config"); + last; + } + my $config_rs = $c->model('DB')->resultset('autoprov_configs')->search({ + id => $resource->{config_id}, + }); + if($c->user->roles eq "admin") { + } elsif($c->user->roles eq "reseller") { + $config_rs = $config_rs->search({ + 'device.reseller_id' => $c->user->reseller_id, + },{ + join => 'device', + }); + } + my $config = $config_rs->first; + unless($config) { + $c->log->error("Pbx device config with confg_id '$$resource{config_id}' does not exist"); + $self->error($c, HTTP_UNPROCESSABLE_ENTITY, "Pbx device config does not exist"); + last; + } + + $item->update($resource); + + return $item; +} + + 1; # vim: set tabstop=4 expandtab: