API implementation (#712)

* initial v0 creator

* working json api for generic reports

* add require login

* move report swagger into plugin, and set new default layout of noop

* require proper role and also use new util func

* start to tidy authn

* some work on cleaning up web authn

* clean up the authN checks

* fix bug

* fix the auth for api

* fixes to json handling

* set swagger sort order

* enable most reports for api endpoints

* fix doc

* add paramters to reports

* add missed report

* allow api_parameters in reports config

* reorganise api

* add vlan search

* add port search

* make sure to enable layout processing

* add device search

* add v1 to api paths

* add Node Search

* support api_responses

* add device object search; fix spurious ports field in device result class

* handle some plugins just returning undef if search fails

* errors from api seamlessley

* fix error in date range default

* more sensible default for prefix

* change order of endpoints in swagger-ui

* all db row classes can now TO_JSON

* add device_port api endpoint

* add device ports endpoint

* do not expand docs

* add swagger ui json tree formatter

* add all relations from Device table

* add port relations

* add nodes retrieve on device or vlan

* rename to GetAPIKey

* update config for previous commit
This commit is contained in:
Oliver Gorwits
2020-04-15 21:15:52 +01:00
committed by GitHub
parent a8a77a2df1
commit dff26abc5c
78 changed files with 815 additions and 257 deletions

View File

@@ -35,8 +35,8 @@ Module::Build->new(
'DBIx::Class::Helpers' => '2.033004', 'DBIx::Class::Helpers' => '2.033004',
'Daemon::Control' => '0.001006', 'Daemon::Control' => '0.001006',
'Dancer' => '1.3132', 'Dancer' => '1.3132',
'Dancer::Plugin::DBIC' => '0.2001',
'Dancer::Plugin::Auth::Extensible' => '0.30', 'Dancer::Plugin::Auth::Extensible' => '0.30',
'Dancer::Plugin::DBIC' => '0.2001',
'Dancer::Plugin::Passphrase' => '2.0.1', 'Dancer::Plugin::Passphrase' => '2.0.1',
'Dancer::Plugin::Swagger' => '0', 'Dancer::Plugin::Swagger' => '0',
'Dancer::Session::Cookie' => '0.27', 'Dancer::Session::Cookie' => '0.27',

View File

@@ -0,0 +1,20 @@
package App::Netdisco::DB::Result;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->load_components(qw{Helper::Row::ToJSON});
# for DBIx::Class::Helper::Row::ToJSON
# to allow text columns to be included in results
sub unserializable_data_types {
return {
blob => 1,
ntext => 1,
};
}
1;

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::Admin;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("admin"); __PACKAGE__->table("admin");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"job", "job",

View File

@@ -4,7 +4,7 @@ package App::Netdisco::DB::Result::Community;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("community"); __PACKAGE__->table("community");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -1,7 +1,6 @@
use utf8; use utf8;
package App::Netdisco::DB::Result::Device; package App::Netdisco::DB::Result::Device;
use strict; use strict;
use warnings; use warnings;
@@ -10,7 +9,7 @@ use App::Netdisco::Util::DNS 'hostname_from_ip';
use overload '""' => sub { shift->ip }, fallback => 1; use overload '""' => sub { shift->ip }, fallback => 1;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device"); __PACKAGE__->table("device");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",
@@ -36,8 +35,6 @@ __PACKAGE__->add_columns(
{ data_type => "text", is_nullable => 1 }, { data_type => "text", is_nullable => 1 },
"layers", "layers",
{ data_type => "varchar", is_nullable => 1, size => 8 }, { data_type => "varchar", is_nullable => 1, size => 8 },
"ports",
{ data_type => "integer", is_nullable => 1 },
"mac", "mac",
{ data_type => "macaddr", is_nullable => 1 }, { data_type => "macaddr", is_nullable => 1 },
"serial", "serial",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DeviceIp;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_ip"); __PACKAGE__->table("device_ip");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DeviceModule;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_module"); __PACKAGE__->table("device_module");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -9,7 +9,7 @@ use NetAddr::MAC;
use MIME::Base64 'encode_base64url'; use MIME::Base64 'encode_base64url';
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port"); __PACKAGE__->table("device_port");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DevicePortLog;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port_log"); __PACKAGE__->table("device_port_log");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"id", "id",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DevicePortPower;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port_power"); __PACKAGE__->table("device_port_power");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -4,7 +4,7 @@ package App::Netdisco::DB::Result::DevicePortProperties;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port_properties"); __PACKAGE__->table("device_port_properties");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DevicePortSsid;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port_ssid"); __PACKAGE__->table("device_port_ssid");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DevicePortVlan;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port_vlan"); __PACKAGE__->table("device_port_vlan");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DevicePortWireless;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_port_wireless"); __PACKAGE__->table("device_port_wireless");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DevicePower;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_power"); __PACKAGE__->table("device_power");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -6,7 +6,7 @@ use warnings;
use List::MoreUtils (); use List::MoreUtils ();
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_skip"); __PACKAGE__->table("device_skip");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"backend", "backend",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::DeviceVlan;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("device_vlan"); __PACKAGE__->table("device_vlan");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"ip", "ip",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::Log;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("log"); __PACKAGE__->table("log");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"id", "id",

View File

@@ -4,7 +4,7 @@ package App::Netdisco::DB::Result::NetmapPositions;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("netmap_positions"); __PACKAGE__->table("netmap_positions");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"id", "id",

View File

@@ -7,7 +7,7 @@ use warnings;
use NetAddr::MAC; use NetAddr::MAC;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("node"); __PACKAGE__->table("node");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"mac", "mac",
@@ -19,7 +19,7 @@ __PACKAGE__->add_columns(
"active", "active",
{ data_type => "boolean", is_nullable => 1 }, { data_type => "boolean", is_nullable => 1 },
"oui", "oui",
{ data_type => "varchar", is_nullable => 1, size => 8 }, { data_type => "varchar", is_nullable => 1, is_serializable => 0, size => 8 },
"time_first", "time_first",
{ {
data_type => "timestamp", data_type => "timestamp",

View File

@@ -7,7 +7,7 @@ use warnings;
use NetAddr::MAC; use NetAddr::MAC;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("node_ip"); __PACKAGE__->table("node_ip");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"mac", "mac",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::NodeMonitor;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("node_monitor"); __PACKAGE__->table("node_monitor");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"mac", "mac",

View File

@@ -7,7 +7,7 @@ use warnings;
use NetAddr::MAC; use NetAddr::MAC;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("node_nbt"); __PACKAGE__->table("node_nbt");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"mac", "mac",

View File

@@ -7,7 +7,7 @@ use warnings;
use NetAddr::MAC; use NetAddr::MAC;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("node_wireless"); __PACKAGE__->table("node_wireless");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"mac", "mac",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::Oui;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("oui"); __PACKAGE__->table("oui");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"oui", "oui",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::Process;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("process"); __PACKAGE__->table("process");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"controller", "controller",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::Session;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("sessions"); __PACKAGE__->table("sessions");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"id", "id",

View File

@@ -4,7 +4,7 @@ package App::Netdisco::DB::Result::Statistics;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("statistics"); __PACKAGE__->table("statistics");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"day", "day",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::Subnet;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("subnets"); __PACKAGE__->table("subnets");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"net", "net",

View File

@@ -4,7 +4,7 @@ package App::Netdisco::DB::Result::Topology;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("topology"); __PACKAGE__->table("topology");

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::User;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("users"); __PACKAGE__->table("users");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"username", "username",

View File

@@ -5,7 +5,7 @@ package App::Netdisco::DB::Result::UserLog;
use strict; use strict;
use warnings; use warnings;
use base 'DBIx::Class::Core'; use base 'App::Netdisco::DB::Result';
__PACKAGE__->table("user_log"); __PACKAGE__->table("user_log");
__PACKAGE__->add_columns( __PACKAGE__->add_columns(
"entry", "entry",

View File

@@ -28,6 +28,7 @@ __PACKAGE__->result_source_instance->view_definition(<<ENDSQL
UNION UNION
SELECT username, 'api' AS role FROM users SELECT username, 'api' AS role FROM users
WHERE token IS NOT NULL AND token_from IS NOT NULL WHERE token IS NOT NULL AND token_from IS NOT NULL
AND token_from > (EXTRACT(EPOCH FROM now()) - ?)
ENDSQL ENDSQL
); );

View File

@@ -206,6 +206,10 @@ Can match any of the Device IP address aliases as a substring.
Can be a string IP or a NetAddr::IP object, either way being treated as an Can be a string IP or a NetAddr::IP object, either way being treated as an
IPv4 or IPv6 prefix within which the device must have one IP address alias. IPv4 or IPv6 prefix within which the device must have one IP address alias.
=item layers
OSI Layers which the device must support.
=back =back
=cut =cut

View File

@@ -3,12 +3,20 @@ package App::Netdisco::Util::Web;
use strict; use strict;
use warnings; use warnings;
use base 'Exporter'; use Dancer ':syntax';
use Time::Piece; use Time::Piece;
use Time::Seconds; use Time::Seconds;
use base 'Exporter';
our @EXPORT = (); our @EXPORT = ();
our @EXPORT_OK = qw/ our @EXPORT_OK = qw/
sort_port sort_modules interval_to_daterange sql_match sort_port sort_modules
interval_to_daterange
sql_match
request_is_api
request_is_api_report
request_is_api_search
/; /;
our %EXPORT_TAGS = (all => \@EXPORT_OK); our %EXPORT_TAGS = (all => \@EXPORT_OK);
@@ -25,6 +33,51 @@ subroutines.
=head1 EXPORT_OK =head1 EXPORT_OK
=head2 request_is_api
Client has requested JSON format data and an endpoint under C</api>.
=cut
sub request_is_api {
return ((request->accept =~ m/(?:json|javascript)/) and (
index(request->path, uri_for('/api/')->path) == 0
or
(param('return_url')
and index(param('return_url'), uri_for('/api/')->path) == 0)
));
}
=head2 request_is_api_report
Same as C<request_is_api> but also requires path to start "C</api/v1/report/...>".
=cut
sub request_is_api_report {
return (request_is_api and (
index(request->path, uri_for('/api/v1/report/')->path) == 0
or
(param('return_url')
and index(param('return_url'), uri_for('/api/v1/report/')->path) == 0)
));
}
=head2 request_is_api_search
Same as C<request_is_api> but also requires path to start "C</api/v1/search/...>".
=cut
sub request_is_api_search {
return (request_is_api and (
index(request->path, uri_for('/api/v1/search/')->path) == 0
or
(param('return_url')
and index(param('return_url'), uri_for('/api/v1/search/')->path) == 0)
));
}
=head2 sql_match( $value, $exact? ) =head2 sql_match( $value, $exact? )
Convert wildcard characters "C<*>" and "C<?>" to "C<%>" and "C<_>" Convert wildcard characters "C<*>" and "C<?>" to "C<%>" and "C<_>"

View File

@@ -7,19 +7,55 @@ use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible; use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Swagger; use Dancer::Plugin::Swagger;
use Dancer::Error;
use Dancer::Continuation::Route::ErrorSent;
use URI (); use URI ();
use Socket6 (); # to ensure dependency is met use Socket6 (); # to ensure dependency is met
use HTML::Entities (); # to ensure dependency is met use HTML::Entities (); # to ensure dependency is met
use URI::QueryParam (); # part of URI, to add helper methods use URI::QueryParam (); # part of URI, to add helper methods
use Path::Class 'dir'; use Path::Class 'dir';
use Module::Load (); use Module::Load ();
use App::Netdisco::Util::Web 'interval_to_daterange'; use App::Netdisco::Util::Web qw/
interval_to_daterange
request_is_api
request_is_api_report
request_is_api_search
/;
BEGIN {
# https://github.com/PerlDancer/Dancer/issues/967
no warnings 'redefine';
*Dancer::_redirect = sub {
my ($destination, $status) = @_;
my $response = Dancer::SharedData->response;
$response->status($status || 302);
$response->headers('Location' => $destination);
};
# neater than using Dancer::Plugin::Res to handle JSON differently
*Dancer::send_error = sub {
my ($body, $status) = @_;
if (request_is_api) {
status $status || 400;
$body = '' unless defined $body;
Dancer::Continuation::Route::ErrorSent->new(
return_value => to_json { error => $body, return_url => param('return_url') }
)->throw;
}
Dancer::Continuation::Route::ErrorSent->new(
return_value => Dancer::Error->new(
message => $body,
code => $status || 500)->render()
)->throw;
};
}
use App::Netdisco::Web::AuthN; use App::Netdisco::Web::AuthN;
use App::Netdisco::Web::Static; use App::Netdisco::Web::Static;
use App::Netdisco::Web::Search; use App::Netdisco::Web::Search;
use App::Netdisco::Web::Device; use App::Netdisco::Web::Device;
use App::Netdisco::Web::Report; use App::Netdisco::Web::Report;
use App::Netdisco::Web::API::Objects;
use App::Netdisco::Web::AdminTask; use App::Netdisco::Web::AdminTask;
use App::Netdisco::Web::TypeAhead; use App::Netdisco::Web::TypeAhead;
use App::Netdisco::Web::PortControl; use App::Netdisco::Web::PortControl;
@@ -82,13 +118,14 @@ $swagger->{schemes} = ['http','https'];
$swagger->{consumes} = 'application/json'; $swagger->{consumes} = 'application/json';
$swagger->{produces} = 'application/json'; $swagger->{produces} = 'application/json';
$swagger->{tags} = [ $swagger->{tags} = [
{name => 'Global'}, {name => 'General',
{name => 'Devices', description => 'Log in and Log out'},
description => 'Operations relating to Devices (switches, routers, etc)'}, {name => 'Search',
{name => 'Nodes', description => 'Search Operations'},
description => 'Operations relating to Nodes (end-stations such as printers)'}, {name => 'Objects',
{name => 'NodeIPs', description => 'Retrieve Device, Port, and associated Node Data'},
description => 'Operations relating to MAC-IP mappings (IPv4 ARP and IPv6 Neighbors)'}, {name => 'Reports',
description => 'Canned and Custom Reports'},
]; ];
$swagger->{securityDefinitions} = { $swagger->{securityDefinitions} = {
APIKeyHeader => APIKeyHeader =>
@@ -229,6 +266,15 @@ hook 'after_template_render' => sub {
# debug $template_engine->{config}->{AUTO_FILTER}; # debug $template_engine->{config}->{AUTO_FILTER};
}; };
# support for report api which is basic table result in json
hook before_layout_render => sub {
my ($tokens, $html_ref) = @_;
return unless request_is_api_report or request_is_api_search;
${ $html_ref } =
$tokens->{results} ? (to_json $tokens->{results}) : {};
};
# workaround for Swagger plugin weird response body # workaround for Swagger plugin weird response body
hook 'after' => sub { hook 'after' => sub {
my $r = shift; # a Dancer::Response my $r = shift; # a Dancer::Response
@@ -237,6 +283,13 @@ hook 'after' => sub {
$r->content( to_json( $r->content ) ); $r->content( to_json( $r->content ) );
header('Content-Type' => 'application/json'); header('Content-Type' => 'application/json');
} }
# instead of setting serialiser
# and also to handle some plugins just returning undef if search fails
if (request_is_api) {
header('Content-Type' => 'application/json');
$r->content( $r->content || '[]' );
}
}; };
# remove empty lines from CSV response # remove empty lines from CSV response
@@ -262,15 +315,4 @@ any qr{.*} => sub {
template 'index'; template 'index';
}; };
{
# https://github.com/PerlDancer/Dancer/issues/967
no warnings 'redefine';
*Dancer::_redirect = sub {
my ($destination, $status) = @_;
my $response = Dancer::SharedData->response;
$response->status($status || 302);
$response->headers('Location' => $destination);
};
}
true; true;

View File

@@ -0,0 +1,176 @@
package App::Netdisco::Web::API::Objects;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Swagger;
use Dancer::Plugin::Auth::Extensible;
use Try::Tiny;
swagger_path {
tags => ['Objects'],
description => 'Returns a row from the device table',
parameters => [
ip => {
description => 'Canonical IP of the Device. Use Search methods to find this.',
required => 1,
in => 'path',
},
],
responses => { default => {} },
}, get '/api/v1/object/device/:ip' => require_role api => sub {
my $device = try { schema('netdisco')->resultset('Device')
->find( params->{ip} ) } or send_error('Bad Device', 404);
return to_json $device->TO_JSON;
};
foreach my $rel (qw/device_ips vlans ports modules port_vlans wireless_ports ssids powered_ports/) {
swagger_path {
tags => ['Objects'],
description => "Returns $rel rows for a given device",
parameters => [
ip => {
description => 'Canonical IP of the Device. Use Search methods to find this.',
required => 1,
in => 'path',
},
],
responses => { default => {} },
}, get "/api/v1/object/device/:ip/$rel" => require_role api => sub {
my $rows = try { schema('netdisco')->resultset('Device')
->find( params->{ip} )->$rel } or send_error('Bad Device', 404);
return to_json [ map {$_->TO_JSON} $rows->all ];
};
}
swagger_path {
tags => ['Objects'],
description => 'Returns a row from the device_port table',
path => '/api/v1/object/device/{ip}/port/{port}',
parameters => [
ip => {
description => 'Canonical IP of the Device. Use Search methods to find this.',
required => 1,
in => 'path',
},
port => {
description => 'Name of the port. Use the ".../device/{ip}/ports" method to find these.',
required => 1,
in => 'path',
},
],
responses => { default => {} },
}, get qr{/api/v1/object/device/(?<ip>[^/]+)/port/(?<port>[^/]+)$} => require_role api => sub {
my $params = captures;
my $port = try { schema('netdisco')->resultset('DevicePort')
->find( $$params{port}, $$params{ip} ) }
or send_error('Bad Device or Port', 404);
return to_json $port->TO_JSON;
};
foreach my $rel (qw/nodes active_nodes nodes_with_age active_nodes_with_age vlans logs/) {
swagger_path {
tags => ['Objects'],
description => "Returns $rel rows for a given port",
path => "/api/v1/object/device/{ip}/port/{port}/$rel",
parameters => [
ip => {
description => 'Canonical IP of the Device. Use Search methods to find this.',
required => 1,
in => 'path',
},
port => {
description => 'Name of the port. Use the ".../device/{ip}/ports" method to find these.',
required => 1,
in => 'path',
},
],
responses => { default => {} },
}, get qq{/api/v1/object/device/(?<ip>[^/]+)/port/(?<port>[^/]+)/$rel} => require_role api => sub {
my $params = captures;
my $rows = try { schema('netdisco')->resultset('DevicePort')
->find( $$params{port}, $$params{ip} )->$rel }
or send_error('Bad Device or Port', 404);
return to_json [ map {$_->TO_JSON} $rows->all ];
};
}
foreach my $rel (qw/power properties ssid wireless agg_master neighbor last_node/) {
swagger_path {
tags => ['Objects'],
description => "Returns the related $rel table entry for a given port",
path => "/api/v1/object/device/{ip}/port/{port}/$rel",
parameters => [
ip => {
description => 'Canonical IP of the Device. Use Search methods to find this.',
required => 1,
in => 'path',
},
port => {
description => 'Name of the port. Use the ".../device/{ip}/ports" method to find these.',
required => 1,
in => 'path',
},
],
responses => { default => {} },
}, get qq{/api/v1/object/device/(?<ip>[^/]+)/port/(?<port>[^/]+)/$rel} => require_role api => sub {
my $params = captures;
my $row = try { schema('netdisco')->resultset('DevicePort')
->find( $$params{port}, $$params{ip} )->$rel }
or send_error('Bad Device or Port', 404);
return to_json $row->TO_JSON;
};
}
swagger_path {
tags => ['Objects'],
description => "Returns the nodes found on a given Device",
parameters => [
ip => {
description => 'Canonical IP of the Device. Use Search methods to find this.',
required => 1,
in => 'path',
},
active_only => {
description => 'Restrict results to active Nodes only',
type => 'boolean',
default => 'true',
in => 'query',
},
],
responses => { default => {} },
}, get '/api/v1/object/device/:ip/nodes' => require_role api => sub {
my $active = (params->{active_only} and ('true' eq params->{active_only})) ? 1 : 0;
my $rows = try { schema('netdisco')->resultset('Node')
->search({ switch => params->{ip}, ($active ? (-bool => 'active') : ()) }) }
or send_error('Bad Device', 404);
return to_json [ map {$_->TO_JSON} $rows->all ];
};
swagger_path {
tags => ['Objects'],
description => "Returns the nodes found in a given VLAN",
parameters => [
vlan => {
description => 'VLAN number',
type => 'integer',
required => 1,
in => 'path',
},
active_only => {
description => 'Restrict results to active Nodes only',
type => 'boolean',
default => 'true',
in => 'query',
},
],
responses => { default => {} },
}, get '/api/v1/object/vlan/:vlan/nodes' => require_role api => sub {
my $active = (params->{active_only} and ('true' eq params->{active_only})) ? 1 : 0;
my $rows = try { schema('netdisco')->resultset('Node')
->search({ vlan => params->{vlan}, ($active ? (-bool => 'active') : ()) }) }
or send_error('Bad VLAN', 404);
return to_json [ map {$_->TO_JSON} $rows->all ];
};
true;

View File

@@ -66,12 +66,12 @@ get '/admin/*' => require_role admin => sub {
var(nav => 'admin'); var(nav => 'admin');
template 'admintask', { template 'admintask', {
task => setting('_admin_tasks')->{ $tag }, task => setting('_admin_tasks')->{ $tag },
}; }, { layout => 'main' };
} }
else { else {
var('notfound' => true); var('notfound' => true);
status 'not_found'; status 'not_found';
template 'index'; template 'index', {}, { layout => 'main' };
} }
}; };

View File

@@ -94,7 +94,8 @@ sub get_user_roles {
my $role_column = $settings->{role_column} || 'role'; my $role_column = $settings->{role_column} || 'role';
return [ try { return [ try {
$user->$roles->get_column( $role_column )->all; $user->$roles->search({}, { bind => [setting('api_token_lifetime')] })
->get_column( $role_column )->all;
} ]; } ];
} }

View File

@@ -5,26 +5,49 @@ use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible; use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Swagger; use Dancer::Plugin::Swagger;
use App::Netdisco::Util::Web 'request_is_api';
use MIME::Base64; use MIME::Base64;
sub request_is_api { # ensure that regardless of where the user is redirected, we have a link
return (setting('api_token_lifetime') # back to the page they requested.
and request->header('Authorization')
and request->accept =~ m/(?:json|javascript)/);
}
hook 'before' => sub { hook 'before' => sub {
params->{return_url} ||= ((request->path ne uri_for('/')->path) params->{return_url} ||= ((request->path ne uri_for('/')->path)
? request->uri : uri_for(setting('web_home'))->path); ? request->uri : uri_for(setting('web_home'))->path);
};
# Dancer will create a session if it sees its own cookie. For the API and also
# various auto login options we need to bootstrap the session instead. If no
# auth data passed, then the hook simply returns, no session is set, and the
# user is redirected to login page.
hook 'before' => sub {
# return if request is for endpoints not requiring a session
return if (
request->path eq uri_for('/login')->path
or request->path eq uri_for('/logout')->path
or request->path eq uri_for('/swagger.json')->path
or index(request->path, uri_for('/swagger-ui')->path) == 0
);
# from the internals of Dancer::Plugin::Auth::Extensible # from the internals of Dancer::Plugin::Auth::Extensible
my $provider = Dancer::Plugin::Auth::Extensible::auth_provider('users'); my $provider = Dancer::Plugin::Auth::Extensible::auth_provider('users');
if (! session('logged_in_user') # API calls must conform strictly to path and header requirements
and request->path ne uri_for('/login')->path if (request_is_api) {
and request->path ne uri_for('/logout')->path # Dancer will issue a cookie to the client which could be returned and
and request->path ne uri_for('/swagger.json')->path # cause API calls to succeed without passing token. Kill the session.
and index(request->path, uri_for('/swagger-ui')->path) != 0) { session->destroy;
my $token = request->header('Authorization');
my $user = $provider->validate_api_token($token)
or return;
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
return;
}
# after checking API, we can short circuit if Dancer reads its cookie OK
return if session('logged_in_user');
if (setting('trust_x_remote_user') if (setting('trust_x_remote_user')
and scalar request->header('X-REMOTE_USER') and scalar request->header('X-REMOTE_USER')
@@ -52,52 +75,25 @@ hook 'before' => sub {
session(logged_in_user => 'guest'); session(logged_in_user => 'guest');
session(logged_in_user_realm => 'users'); session(logged_in_user_realm => 'users');
} }
elsif (request_is_api()
and index(request->path, uri_for('/api')->path) == 0) {
my $token = request->header('Authorization');
my $user = $provider->validate_api_token($token)
or return;
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
}
else { else {
# user has no AuthN - force to handler for '/' # user has no AuthN - force to handler for '/'
request->path_info('/'); request->path_info('/');
} }
}
};
# user redirected here (POST -> GET) when login fails
get qr{^/(?:login(?:/denied)?)?} => sub {
if (request_is_api()) {
status('unauthorized');
return to_json {
error => 'not authorized',
return_url => param('return_url'),
};
}
else {
template 'index', { return_url => param('return_url') };
}
}; };
# override default login_handler so we can log access in the database # override default login_handler so we can log access in the database
swagger_path { swagger_path {
description => 'Obtain an API Key using HTTP BasicAuth', description => 'Obtain an API Key',
tags => ['Global'], tags => ['General'],
parameters => [], parameters => [],
responses => { responses => { default => { examples => {
default => {
examples => {
'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } }, 'application/json' => { api_key => 'cc9d5c02d8898e5728b7d7a0339c0785' } } },
}, },
}, },
post '/login' => sub { post '/login' => sub {
my $mode = (request_is_api() ? 'API' : 'WebUI'); my $api = ((request->accept =~ m/(?:json|javascript)/) ? true : false);
# get authN data from request (HTTP BasicAuth or Form params) # get authN data from BasicAuth header used by API, put into params
my $authheader = request->header('Authorization'); my $authheader = request->header('Authorization');
if (defined $authheader and $authheader =~ /^Basic (.*)$/i) { if (defined $authheader and $authheader =~ /^Basic (.*)$/i) {
my ($u, $p) = split(m/:/, (MIME::Base64::decode($1) || ":")); my ($u, $p) = split(m/:/, (MIME::Base64::decode($1) || ":"));
@@ -119,12 +115,13 @@ post '/login' => sub {
schema('netdisco')->resultset('UserLog')->create({ schema('netdisco')->resultset('UserLog')->create({
username => session('logged_in_user'), username => session('logged_in_user'),
userip => request->remote_address, userip => request->remote_address,
event => "Login ($mode)", event => (sprintf 'Login (%s)', ($api ? 'API' : 'WebUI')),
details => param('return_url'), details => param('return_url'),
}); });
$user->update({ last_on => \'now()' }); $user->update({ last_on => \'now()' });
if ($mode eq 'API') { if ($api) {
header('Content-Type' => 'application/json');
$user->update({ $user->update({
token_from => time, token_from => time,
token => \'md5(random()::text)', token => \'md5(random()::text)',
@@ -135,16 +132,18 @@ post '/login' => sub {
redirect param('return_url'); redirect param('return_url');
} }
else { else {
# invalidate session cookie
session->destroy; session->destroy;
schema('netdisco')->resultset('UserLog')->create({ schema('netdisco')->resultset('UserLog')->create({
username => param('username'), username => param('username'),
userip => request->remote_address, userip => request->remote_address,
event => "Login Failure ($mode)", event => (sprintf 'Login Failure (%s)', ($api ? 'API' : 'WebUI')),
details => param('return_url'), details => param('return_url'),
}); });
if ($mode eq 'API') { if ($api) {
header('Content-Type' => 'application/json');
status('unauthorized'); status('unauthorized');
return to_json { error => 'authentication failed' }; return to_json { error => 'authentication failed' };
} }
@@ -164,12 +163,12 @@ Dancer::Plugin::Swagger->instance->doc->{paths}->{'/login'}
# we override the default login_handler, so logout has to be handled as well # we override the default login_handler, so logout has to be handled as well
swagger_path { swagger_path {
description => 'Destroy user API Key and session cookie', description => 'Destroy user API Key and session cookie',
tags => ['Global'], tags => ['General'],
parameters => [], parameters => [],
responses => { default => { examples => { 'application/json' => {} } } }, responses => { default => { examples => { 'application/json' => {} } } },
}, },
get '/logout' => sub { get '/logout' => sub {
my $mode = (request_is_api() ? 'API' : 'WebUI'); my $api = ((request->accept =~ m/(?:json|javascript)/) ? true : false);
# clear out API token # clear out API token
my $user = schema('netdisco')->resultset('User') my $user = schema('netdisco')->resultset('User')
@@ -183,15 +182,35 @@ get '/logout' => sub {
schema('netdisco')->resultset('UserLog')->create({ schema('netdisco')->resultset('UserLog')->create({
username => session('logged_in_user'), username => session('logged_in_user'),
userip => request->remote_address, userip => request->remote_address,
event => "Logout ($mode)", event => (sprintf 'Logout (%s)', ($api ? 'API' : 'WebUI')),
details => '', details => '',
}); });
if ($mode eq 'API') { if ($api) {
header('Content-Type' => 'application/json');
return to_json {}; return to_json {};
} }
redirect uri_for(setting('web_home'))->path; redirect uri_for(setting('web_home'))->path;
}; };
# user redirected here (POST -> GET) when login fails
get qr{^/(?:login(?:/denied)?)?} => sub {
my $api = ((request->accept =~ m/(?:json|javascript)/) ? true : false);
if ($api) {
header('Content-Type' => 'application/json');
status('unauthorized');
return to_json {
error => 'not authorized',
return_url => param('return_url'),
};
}
else {
template 'index', {
return_url => param('return_url')
}, { layout => 'main' };
}
};
true; true;

View File

@@ -85,7 +85,7 @@ get '/device' => require_login sub {
lgroup_list => [ schema('netdisco')->resultset('Device')->get_distinct_col('location') ], lgroup_list => [ schema('netdisco')->resultset('Device')->get_distinct_col('location') ],
hgroup_list => setting('host_group_displaynames'), hgroup_list => setting('host_group_displaynames'),
device => params->{'tab'}, device => params->{'tab'},
}; }, { layout => 'main' };
}; };
true; true;

View File

@@ -21,6 +21,9 @@ foreach my $report (@{setting('reports')}) {
category => ($report->{category} || 'My Reports'), category => ($report->{category} || 'My Reports'),
($report->{hidden} ? (hidden => true) : ()), ($report->{hidden} ? (hidden => true) : ()),
provides_csv => true, provides_csv => true,
api_endpoint => true,
bind_params => $report->{bind_params},
api_parameters => $report->{api_parameters},
}); });
get "/ajax/content/report/$r" => require_login sub { get "/ajax/content/report/$r" => require_login sub {
@@ -76,16 +79,14 @@ foreach my $report (@{setting('reports')}) {
is_custom_report => true, is_custom_report => true,
column_options => \%column_config, column_options => \%column_config,
headings => [map {$column_config{$_}->{displayname}} @column_order], headings => [map {$column_config{$_}->{displayname}} @column_order],
columns => [@column_order] }, columns => [@column_order] };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/generic_report_csv.tt', template 'ajax/report/generic_report_csv.tt',
{ results => \@results, { results => \@results,
headings => [map {$column_config{$_}->{displayname}} @column_order], headings => [map {$column_config{$_}->{displayname}} @column_order],
columns => [@column_order] }, columns => [@column_order] };
{ layout => undef };
} }
}; };
} }

View File

@@ -19,7 +19,7 @@ sub _make_password {
sub _bail { sub _bail {
var('passchange_failed' => 1); var('passchange_failed' => 1);
return template 'password.tt'; return template 'password.tt', {}, { layout => 'main' };
} }
any ['get', 'post'] => '/password' => require_login sub { any ['get', 'post'] => '/password' => require_login sub {
@@ -45,7 +45,7 @@ any ['get', 'post'] => '/password' => require_login sub {
var('passchange_ok' => 1); var('passchange_ok' => 1);
} }
template 'password.tt'; template 'password.tt', {}, { layout => 'main' };
}; };
true; true;

View File

@@ -2,6 +2,8 @@ package App::Netdisco::Web::Plugin;
use Dancer ':syntax'; use Dancer ':syntax';
use Dancer::Plugin; use Dancer::Plugin;
use Dancer::Plugin::Swagger;
use Dancer::Plugin::Auth::Extensible;
use Path::Class 'dir'; use Path::Class 'dir';
@@ -150,6 +152,19 @@ sub _register_tab {
register 'register_search_tab' => sub { register 'register_search_tab' => sub {
my ($self, $config) = plugin_args(@_); my ($self, $config) = plugin_args(@_);
_register_tab('search', $config); _register_tab('search', $config);
if ($config->{api_endpoint}) {
my $tag = $config->{tag};
swagger_path {
tags => ['Search'],
description => $config->{label} .' Search',
parameters => $config->{api_parameters},
responses =>
($config->{api_responses} || { default => {} }),
}, get "/api/v1/search/$tag" => require_role api => sub {
forward "/ajax/content/search/$tag";
};
}
}; };
register 'register_device_tab' => sub { register 'register_device_tab' => sub {
@@ -178,6 +193,21 @@ register 'register_report' => sub {
if ($config->{tag} eq $tag) { if ($config->{tag} eq $tag) {
setting('_reports')->{$tag} = $config; setting('_reports')->{$tag} = $config;
if ($config->{api_endpoint}) {
(my $category_path = lc $config->{category}) =~ s/ /-/g;
swagger_path {
tags => ['Reports'],
description => $config->{label} .' Report',
parameters =>
($config->{api_parameters} ||
($config->{bind_params} ? [map { $_ => {} } @{ $config->{bind_params} }] : [])),
responses =>
($config->{api_responses} || { default => {} }),
}, get "/api/v1/report/$category_path/$tag" => require_role api => sub {
forward "/ajax/content/report/$tag";
};
}
foreach my $rconfig (@{setting('reports')}) { foreach my $rconfig (@{setting('reports')}) {
if ($rconfig->{tag} eq $tag) { if ($rconfig->{tag} eq $tag) {
setting('_reports')->{$tag}->{'rconfig'} = $rconfig; setting('_reports')->{$tag}->{'rconfig'} = $rconfig;

View File

@@ -29,7 +29,7 @@ get '/inventory' => require_login sub {
template 'inventory', { template 'inventory', {
platforms => [ $platforms->hri->all ], platforms => [ $platforms->hri->all ],
releases => [ @release_list ], releases => [ @release_list ],
}; }, { layout => 'main' };
}; };
true; true;

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'apchanneldist', tag => 'apchanneldist',
label => 'Access Point Channel Distribution', label => 'Access Point Channel Distribution',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -28,13 +29,11 @@ get '/ajax/content/report/apchanneldist' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/apchanneldist.tt', { results => $json }, template 'ajax/report/apchanneldist.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/apchanneldist_csv.tt', { results => \@results }, template 'ajax/report/apchanneldist_csv.tt', { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'apclients', tag => 'apclients',
label => 'Access Point Client Count', label => 'Access Point Client Count',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -37,14 +38,12 @@ get '/ajax/content/report/apclients' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/apclients.tt', { results => $json }, template 'ajax/report/apclients.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/apclients_csv.tt', template 'ajax/report/apclients_csv.tt',
{ results => \@results }, { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -12,6 +12,7 @@ register_report(
tag => 'apradiochannelpower', tag => 'apradiochannelpower',
label => 'Access Point Radios Channel and Power', label => 'Access Point Radios Channel and Power',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -38,8 +39,7 @@ get '/ajax/content/report/apradiochannelpower/data' => require_login sub {
get '/ajax/content/report/apradiochannelpower' => require_login sub { get '/ajax/content/report/apradiochannelpower' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
template 'ajax/report/apradiochannelpower.tt', {}, template 'ajax/report/apradiochannelpower.tt';
{ layout => undef };
} }
else { else {
my @results my @results
@@ -50,8 +50,7 @@ get '/ajax/content/report/apradiochannelpower' => require_login sub {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/apradiochannelpower_csv.tt', template 'ajax/report/apradiochannelpower_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'deviceaddrnodns', tag => 'deviceaddrnodns',
label => 'Addresses without DNS Entries', label => 'Addresses without DNS Entries',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -28,14 +29,12 @@ get '/ajax/content/report/deviceaddrnodns' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/deviceaddrnodns.tt', { results => $json }, template 'ajax/report/deviceaddrnodns.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/deviceaddrnodns_csv.tt', template 'ajax/report/deviceaddrnodns_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'devicebylocation', tag => 'devicebylocation',
label => 'By Location', label => 'By Location',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -24,14 +25,12 @@ get '/ajax/content/report/devicebylocation' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/devicebylocation.tt', { results => $json }, template 'ajax/report/devicebylocation.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/devicebylocation_csv.tt', template 'ajax/report/devicebylocation_csv.tt',
{ results => \@results }, { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'devicednsmismatch', tag => 'devicednsmismatch',
label => 'Device Name / DNS Mismatches', label => 'Device Name / DNS Mismatches',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -27,14 +28,12 @@ get '/ajax/content/report/devicednsmismatch' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/devicednsmismatch.tt', { results => $json }, template 'ajax/report/devicednsmismatch.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/devicednsmismatch_csv.tt', template 'ajax/report/devicednsmismatch_csv.tt',
{ results => \@results }, { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -12,6 +12,7 @@ register_report(
tag => 'devicepoestatus', tag => 'devicepoestatus',
label => 'Power over Ethernet (PoE) Status', label => 'Power over Ethernet (PoE) Status',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -39,7 +40,7 @@ get '/ajax/content/report/devicepoestatus/data' => require_login sub {
get '/ajax/content/report/devicepoestatus' => require_login sub { get '/ajax/content/report/devicepoestatus' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
template 'ajax/report/devicepoestatus.tt', {}, { layout => undef }; template 'ajax/report/devicepoestatus.tt';
} }
else { else {
my @results my @results
@@ -50,8 +51,7 @@ get '/ajax/content/report/devicepoestatus' => require_login sub {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/devicepoestatus_csv.tt', template 'ajax/report/devicepoestatus_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'duplexmismatch', tag => 'duplexmismatch',
label => 'Duplex Mismatches Between Devices', label => 'Duplex Mismatches Between Devices',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -22,14 +23,12 @@ get '/ajax/content/report/duplexmismatch' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/duplexmismatch.tt', { results => $json, }, template 'ajax/report/duplexmismatch.tt', { results => $json, };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/duplexmismatch_csv.tt', template 'ajax/report/duplexmismatch_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,11 +11,11 @@ register_report(
tag => 'halfduplex', tag => 'halfduplex',
label => 'Ports in Half Duplex Mode', label => 'Ports in Half Duplex Mode',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
get '/ajax/content/report/halfduplex' => require_login sub { get '/ajax/content/report/halfduplex' => require_login sub {
my $format = param('format');
my @results my @results
= schema('netdisco')->resultset('DevicePort') = schema('netdisco')->resultset('DevicePort')
->columns( [qw/ ip port name duplex /] )->search( ->columns( [qw/ ip port name duplex /] )->search(
@@ -30,14 +30,12 @@ get '/ajax/content/report/halfduplex' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/halfduplex.tt', { results => $json }, template 'ajax/report/halfduplex.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/halfduplex_csv.tt', template 'ajax/report/halfduplex_csv.tt',
{ results => \@results }, { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -6,12 +6,39 @@ use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin; use App::Netdisco::Web::Plugin;
use NetAddr::IP::Lite ':lower'; use NetAddr::IP::Lite ':lower';
use POSIX qw/strftime/;
register_report( register_report(
{ category => 'IP', { category => 'IP',
tag => 'ipinventory', tag => 'ipinventory',
label => 'IP Inventory', label => 'IP Inventory',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
api_parameters => [
subnet => {
description => 'IP Prefix to search',
required => 1,
},
daterange => {
description => 'Date range to search',
default => ('1970-01-01 to '. strftime('%Y-%m-%d', gmtime)),
},
age_invert => {
description => 'Results should NOT be within daterange',
type => 'boolean',
default => 'false',
},
limit => {
description => 'Maximum number of historical records',
enum => [qw/32 64 128 256 512 1024 2048 4096 8192/],
default => '256',
},
never => {
description => 'Include in the report IPs never seen',
type => 'boolean',
default => 'false',
},
],
} }
); );
@@ -25,7 +52,10 @@ get '/ajax/content/report/ipinventory' => require_login sub {
if (! $subnet) or ($subnet->addr eq '0.0.0.0'); if (! $subnet) or ($subnet->addr eq '0.0.0.0');
my $agenot = param('age_invert') || '0'; my $agenot = param('age_invert') || '0';
my ( $start, $end ) = param('daterange') =~ /(\d+-\d+-\d+)/gmx;
my $daterange = param('daterange')
|| ('1970-01-01 to '. strftime('%Y-%m-%d', gmtime));
my ( $start, $end ) = $daterange =~ /(\d+-\d+-\d+)/gmx;
my $limit = param('limit') || 256; my $limit = param('limit') || 256;
my $never = param('never') || '0'; my $never = param('never') || '0';
@@ -155,13 +185,11 @@ get '/ajax/content/report/ipinventory' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/ipinventory.tt', { results => $json }, template 'ajax/report/ipinventory.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/ipinventory_csv.tt', { results => \@results, }, template 'ajax/report/ipinventory_csv.tt', { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'nodemultiips', tag => 'nodemultiips',
label => 'Nodes with multiple active IP addresses', label => 'Nodes with multiple active IP addresses',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -37,14 +38,12 @@ get '/ajax/content/report/nodemultiips' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/nodemultiips.tt', { results => $json }, template 'ajax/report/nodemultiips.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/nodemultiips_csv.tt', template 'ajax/report/nodemultiips_csv.tt',
{ results => \@results }, { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -12,6 +12,30 @@ register_report(
tag => 'nodesdiscovered', tag => 'nodesdiscovered',
label => 'Nodes discovered through LLDP/CDP', label => 'Nodes discovered through LLDP/CDP',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
api_parameters => [
remote_id => {
description => 'Host Name reported',
},
remote_type => {
description => 'Platform reported',
},
aps => {
description => 'Include Wireless APs in the report',
type => 'boolean',
default => 'false',
},
phones => {
description => 'Include IP Phones in the report',
type => 'boolean',
default => 'false',
},
matchall => {
description => 'Match all parameters (true) or any (false)',
type => 'boolean',
default => 'false',
},
],
} }
); );
@@ -40,14 +64,12 @@ get '/ajax/content/report/nodesdiscovered' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/nodesdiscovered.tt', { results => $json }, template 'ajax/report/nodesdiscovered.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/nodesdiscovered_csv.tt', template 'ajax/report/nodesdiscovered_csv.tt',
{ results => \@results }, { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'portadmindown', tag => 'portadmindown',
label => 'Ports administratively disabled', label => 'Ports administratively disabled',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -31,14 +32,12 @@ get '/ajax/content/report/portadmindown' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/portadmindown.tt', { results => $json }, template 'ajax/report/portadmindown.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portadmindown_csv.tt', template 'ajax/report/portadmindown_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'portblocking', tag => 'portblocking',
label => 'Ports that are blocking', label => 'Ports that are blocking',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -31,14 +32,12 @@ get '/ajax/content/report/portblocking' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/portblocking.tt', { results => $json }, template 'ajax/report/portblocking.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portblocking_csv.tt', template 'ajax/report/portblocking_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,13 @@ register_report(
tag => 'portmultinodes', tag => 'portmultinodes',
label => 'Ports with multiple nodes attached', label => 'Ports with multiple nodes attached',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
api_parameters => [
vlan => {
description => 'Filter by VLAN',
type => 'integer',
},
],
} }
); );
@@ -39,14 +46,12 @@ get '/ajax/content/report/portmultinodes' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/portmultinodes.tt', { results => $json }, template 'ajax/report/portmultinodes.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portmultinodes_csv.tt', template 'ajax/report/portmultinodes_csv.tt',
{ results => \@results, }, { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,12 @@ register_report(
tag => 'portssid', tag => 'portssid',
label => 'Port SSID Inventory', label => 'Port SSID Inventory',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
api_parameters => [
ssid => {
description => 'Get details for this SSID',
},
],
} }
); );
@@ -65,14 +71,12 @@ get '/ajax/content/report/portssid' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/portssid.tt', template 'ajax/report/portssid.tt',
{ results => $json, opt => $ssid }, { results => $json, opt => $ssid };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portssid_csv.tt', template 'ajax/report/portssid_csv.tt',
{ results => \@results, opt => $ssid }, { results => \@results, opt => $ssid };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,19 @@ register_report(
tag => 'portutilization', tag => 'portutilization',
label => 'Port Utilization', label => 'Port Utilization',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
api_parameters => [
age_num => {
description => 'Mark as Free if down for (quantity)',
enum => [1 .. 31],
default => '3',
},
age_unit => {
description => 'Mark as Free if down for (period)',
enum => [qw/days weeks months years/],
default => 'months',
},
],
} }
); );
@@ -24,13 +37,11 @@ get '/ajax/content/report/portutilization' => require_login sub {
if (request->is_ajax) { if (request->is_ajax) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/portutilization.tt', { results => $json }, template 'ajax/report/portutilization.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portutilization_csv.tt', { results => \@results, }, template 'ajax/report/portutilization_csv.tt', { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'portvlanmismatch', tag => 'portvlanmismatch',
label => 'Port VLAN Mismatches', label => 'Port VLAN Mismatches',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -20,13 +21,11 @@ get '/ajax/content/report/portvlanmismatch' => require_login sub {
if (request->is_ajax) { if (request->is_ajax) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/portvlanmismatch.tt', { results => $json }, template 'ajax/report/portvlanmismatch.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portvlanmismatch_csv.tt', { results => \@results, }, template 'ajax/report/portvlanmismatch_csv.tt', { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'ssidinventory', tag => 'ssidinventory',
label => 'SSID Inventory', label => 'SSID Inventory',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -22,13 +23,11 @@ get '/ajax/content/report/ssidinventory' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/report/portssid.tt', { results => $json }, template 'ajax/report/portssid.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portssid_csv.tt', { results => \@results }, template 'ajax/report/portssid_csv.tt', { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -5,19 +5,38 @@ use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible; use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin; use App::Netdisco::Web::Plugin;
use POSIX qw/strftime/;
register_report({ register_report({
category => 'IP', category => 'IP',
tag => 'subnets', tag => 'subnets',
label => 'Subnet Utilization', label => 'Subnet Utilization',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
api_parameters => [
subnet => {
description => 'IP Prefix to search',
default => '0.0.0.0/32',
},
daterange => {
description => 'Date range to search',
default => ('1970-01-01 to '. strftime('%Y-%m-%d', gmtime)),
},
age_invert => {
description => 'Results should NOT be within daterange',
type => 'boolean',
default => 'false',
},
],
}); });
get '/ajax/content/report/subnets' => require_login sub { get '/ajax/content/report/subnets' => require_login sub {
my $subnet = param('subnet') || '0.0.0.0/32'; my $subnet = param('subnet') || '0.0.0.0/32';
my $agenot = param('age_invert') || '0'; my $agenot = param('age_invert') || '0';
my ( $start, $end ) = param('daterange') =~ /(\d+-\d+-\d+)/gmx;
my $daterange = param('daterange')
|| ('1970-01-01 to '. strftime('%Y-%m-%d', gmtime));
my ( $start, $end ) = $daterange =~ /(\d+-\d+-\d+)/gmx;
$start = $start . ' 00:00:00'; $start = $start . ' 00:00:00';
$end = $end . ' 23:59:59'; $end = $end . ' 23:59:59';
@@ -29,13 +48,11 @@ get '/ajax/content/report/subnets' => require_login sub {
return unless scalar @results; return unless scalar @results;
if ( request->is_ajax ) { if ( request->is_ajax ) {
template 'ajax/report/subnets.tt', { results => \@results }, template 'ajax/report/subnets.tt', { results => \@results };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/subnets_csv.tt', { results => \@results }, template 'ajax/report/subnets_csv.tt', { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -11,6 +11,7 @@ register_report(
tag => 'vlaninventory', tag => 'vlaninventory',
label => 'VLAN Inventory', label => 'VLAN Inventory',
provides_csv => 1, provides_csv => 1,
api_endpoint => 1,
} }
); );
@@ -36,13 +37,11 @@ get '/ajax/content/report/vlaninventory' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json (\@results); my $json = to_json (\@results);
template 'ajax/report/vlaninventory.tt', { results => $json }, template 'ajax/report/vlaninventory.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/vlaninventory_csv.tt', { results => \@results }, template 'ajax/report/vlaninventory_csv.tt', { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -8,8 +8,52 @@ use List::MoreUtils ();
use App::Netdisco::Web::Plugin; use App::Netdisco::Web::Plugin;
register_search_tab( register_search_tab({
{ tag => 'device', label => 'Device', provides_csv => 1 } ); tag => 'device',
label => 'Device',
provides_csv => 1,
api_endpoint => 1,
api_parameters => [
q => {
description => 'Partial match of Device contact, serial, module serials, location, name, description, dns, or any IP alias',
},
name => {
description => 'Partial match of the Device name',
},
location => {
description => 'Partial match of the Device location',
},
dns => {
description => 'Partial match of any of the Device IP aliases',
},
ip => {
description => 'IP or IP Prefix within which the Device must have an interface address',
},
description => {
description => 'Partial match of the Device description',
},
model => {
description => 'Exact match of the Device model',
},
os => {
description => 'Exact match of the Device operating system',
},
os_ver => {
description => 'Exact match of the Device operating system version',
},
vendor => {
description => 'Exact match of the Device vendor',
},
layers => {
description => 'OSI Layer which the device must support',
},
matchall => {
description => 'If true, all fields (except "q") must match the Device',
type => 'boolean',
default => 'false',
},
],
});
# device with various properties or a default match-all # device with various properties or a default match-all
get '/ajax/content/search/device' => require_login sub { get '/ajax/content/search/device' => require_login sub {
@@ -40,13 +84,11 @@ get '/ajax/content/search/device' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/search/device.tt', { results => $json }, template 'ajax/search/device.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/search/device_csv.tt', { results => \@results, }, template 'ajax/search/device_csv.tt', { results => \@results, };
{ layout => undef };
} }
}; };

View File

@@ -12,7 +12,51 @@ use NetAddr::MAC ();
use App::Netdisco::Web::Plugin; use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Web 'sql_match'; use App::Netdisco::Util::Web 'sql_match';
register_search_tab({ tag => 'node', label => 'Node' }); register_search_tab({
tag => 'node',
label => 'Node',
api_endpoint => 1,
api_parameters => [
q => {
description => 'MAC Address or IP Address or Hostname (without Domain Suffix) of a Node (supports SQL or "*" wildcards)',
required => 1,
},
partial => {
description => 'Partially match the "q" parameter (wildcard characters not required)',
type => 'boolean',
default => 'false',
},
deviceports => {
description => 'MAC Address search will include Device Port MACs',
type => 'boolean',
default => 'true',
},
show_vendor => {
description => 'Include interface Vendor in results',
type => 'boolean',
default => 'false',
},
archived => {
description => 'Include archived records in results',
type => 'boolean',
default => 'false',
},
daterange => {
description => 'Date Range in format "YYYY-MM-DD to YYYY-MM-DD"',
},
age_invert => {
description => 'Date Range is NOT within the supplied range',
type => 'boolean',
default => 'false',
},
# mac_format is used only in the template (will be IEEE) in results
#mac_format => {
#},
# stamps param is used only in the template (they will be included)
#stamps => {
#},
],
});
# nodes matching the param as an IP or DNS hostname or MAC # nodes matching the param as an IP or DNS hostname or MAC
ajax '/ajax/content/search/node' => require_login sub { ajax '/ajax/content/search/node' => require_login sub {
@@ -144,7 +188,7 @@ ajax '/ajax/content/search/node' => require_login sub {
ports => $ports, ports => $ports,
wireless => $wireless, wireless => $wireless,
netbios => $netbios, netbios => $netbios,
}, { layout => undef }; };
} }
else { else {
my $ports = param('deviceports') my $ports = param('deviceports')
@@ -157,7 +201,7 @@ ajax '/ajax/content/search/node' => require_login sub {
ports => $ports, ports => $ports,
wireless => $wireless, wireless => $wireless,
netbios => $netbios, netbios => $netbios,
}, { layout => undef }; };
} }
} }
@@ -200,7 +244,7 @@ ajax '/ajax/content/search/node' => require_login sub {
template 'ajax/search/node_by_ip.tt', { template 'ajax/search/node_by_ip.tt', {
macs => $set, macs => $set,
archive_filter => {@active}, archive_filter => {@active},
}, { layout => undef }; };
}; };
true; true;

View File

@@ -7,7 +7,33 @@ use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin; use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Web 'sql_match'; use App::Netdisco::Util::Web 'sql_match';
register_search_tab( { tag => 'port', label => 'Port', provides_csv => 1 } ); register_search_tab({
tag => 'port',
label => 'Port',
provides_csv => 1,
api_endpoint => 1,
api_parameters => [
q => {
description => 'Port name or VLAN or MAC address',
required => 1,
},
partial => {
description => 'Search for a partial match on parameter "q"',
type => 'boolean',
default => 'true',
},
uplink => {
description => 'Include uplinks in results',
type => 'boolean',
default => 'false',
},
ethernet => {
description => 'Only Ethernet type interfaces in results',
type => 'boolean',
default => 'true',
},
],
});
# device ports with a description (er, name) matching # device ports with a description (er, name) matching
get '/ajax/content/search/port' => require_login sub { get '/ajax/content/search/port' => require_login sub {
@@ -67,13 +93,11 @@ get '/ajax/content/search/port' => require_login sub {
if ( request->is_ajax ) { if ( request->is_ajax ) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/search/port.tt', { results => $json }, template 'ajax/search/port.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/search/port_csv.tt', { results => \@results }, template 'ajax/search/port_csv.tt', { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -6,7 +6,18 @@ use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin; use App::Netdisco::Web::Plugin;
register_search_tab( { tag => 'vlan', label => 'VLAN', provides_csv => 1 } ); register_search_tab({
tag => 'vlan',
label => 'VLAN',
provides_csv => 1,
api_endpoint => 1,
api_parameters => [
q => {
description => 'VLAN name or number',
required => 1,
},
],
});
# devices carrying vlan xxx # devices carrying vlan xxx
get '/ajax/content/search/vlan' => require_login sub { get '/ajax/content/search/vlan' => require_login sub {
@@ -28,13 +39,11 @@ get '/ajax/content/search/vlan' => require_login sub {
if (request->is_ajax) { if (request->is_ajax) {
my $json = to_json( \@results ); my $json = to_json( \@results );
template 'ajax/search/vlan.tt', { results => $json }, template 'ajax/search/vlan.tt', { results => $json };
{ layout => undef };
} }
else { else {
header( 'Content-Type' => 'text/comma-separated-values' ); header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/search/vlan_csv.tt', { results => \@results }, template 'ajax/search/vlan_csv.tt', { results => \@results };
{ layout => undef };
} }
}; };

View File

@@ -51,7 +51,7 @@ get '/report/*' => require_login sub {
ssid_list => $ssid_list, ssid_list => $ssid_list,
type_list => $type_list, type_list => $type_list,
vendor_list => $vendor_list, vendor_list => $vendor_list,
}; }, { layout => 'main' };
}; };
true; true;

View File

@@ -95,7 +95,7 @@ get '/search' => require_login sub {
os_list => $os_list, os_list => $os_list,
os_ver_list => $os_ver_list, os_ver_list => $os_ver_list,
vendor_list => $vendor_list, vendor_list => $vendor_list,
}; }, { layout => 'main' };
}; };
true; true;

View File

@@ -1,4 +1,4 @@
package App::Netdisco::Worker::Plugin::SetUserToken; package App::Netdisco::Worker::Plugin::GetAPIKey;
use Dancer ':syntax'; use Dancer ':syntax';
use Dancer::Plugin::DBIC 'schema'; use Dancer::Plugin::DBIC 'schema';
@@ -9,7 +9,7 @@ use aliased 'App::Netdisco::Worker::Status';
register_worker({ phase => 'check' }, sub { register_worker({ phase => 'check' }, sub {
return Status->error('Missing user (-e).') return Status->error('Missing user (-e).')
unless shift->extra; unless shift->extra;
return Status->done('SetUserToken is able to run'); return Status->done('GetAPIKey is able to run');
}); });
register_worker({ phase => 'main' }, sub { register_worker({ phase => 'main' }, sub {

View File

@@ -416,7 +416,7 @@ worker_plugins:
- 'Power' - 'Power'
- 'Psql' - 'Psql'
- 'Renumber' - 'Renumber'
- 'SetUserToken' - 'GetAPIKey'
- 'Show' - 'Show'
- 'Stats' - 'Stats'
- 'Vlan' - 'Vlan'
@@ -494,7 +494,7 @@ engines:
PRE_CHOMP: 1 PRE_CHOMP: 1
INCLUDE_PATH: [] INCLUDE_PATH: []
AUTO_FILTER: 'html_entity' AUTO_FILTER: 'html_entity'
layout: 'main' layout: 'noop'
plugins: plugins:
Swagger: Swagger:
main_api_module: 'App::Netdisco' main_api_module: 'App::Netdisco'

View File

@@ -4,6 +4,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Swagger UI</title> <title>Swagger UI</title>
<script src="./swagger-ui-json-tree-plugin.js"></script>
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" > <link rel="stylesheet" type="text/css" href="./swagger-ui.css" >
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" /> <link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" /> <link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
@@ -37,6 +38,8 @@
<script src="./swagger-ui-standalone-preset.js"> </script> <script src="./swagger-ui-standalone-preset.js"> </script>
<script> <script>
window.onload = function() { window.onload = function() {
// https://github.com/justinebateman/swagger-ui-json-tree-plugin
SwaggerUIStandalonePreset.unshift(jsonTreePlugin.default);
// Begin Swagger UI call region // Begin Swagger UI call region
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: "https://petstore.swagger.io/v2/swagger.json", url: "https://petstore.swagger.io/v2/swagger.json",
@@ -49,7 +52,10 @@
plugins: [ plugins: [
SwaggerUIBundle.plugins.DownloadUrl SwaggerUIBundle.plugins.DownloadUrl
], ],
layout: "StandaloneLayout" layout: "StandaloneLayout",
apisSorter: "alpha",
operationsSorter: "alpha",
docExpansion: "none"
}) })
// End Swagger UI call region // End Swagger UI call region

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
[% content | none %]