relocate repo files so ND2 is the only code

This commit is contained in:
Oliver Gorwits
2017-04-14 23:08:55 +01:00
parent 9a016ea6ba
commit d23b32500f
469 changed files with 0 additions and 6920 deletions

View File

@@ -0,0 +1,71 @@
package App::Netdisco::Web::AdminTask;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use NetAddr::IP qw/:rfc3021 :lower/;
use App::Netdisco::JobQueue 'jq_insert';
use App::Netdisco::Util::Device 'delete_device';
sub add_job {
my ($action, $device, $subaction) = @_;
my $net = NetAddr::IP->new($device);
return if
($device and (!$net or $net->num == 0 or $net->addr eq '0.0.0.0'));
my @hostlist = defined $device ? ($net->hostenum) : (undef);
jq_insert([map {{
($_ ? (device => $_->addr) : ()),
action => $action,
($subaction ? (subaction => $subaction) : ()),
username => session('logged_in_user'),
userip => request->remote_address,
}} @hostlist]);
true;
}
foreach my $action (@{ setting('job_prio')->{high} },
@{ setting('job_prio')->{normal} }) {
ajax "/ajax/control/admin/$action" => require_role admin => sub {
add_job($action, param('device'), param('extra'))
or send_error('Bad device', 400);
};
post "/admin/$action" => require_role admin => sub {
add_job($action, param('device'), param('extra'))
? redirect uri_for('/admin/jobqueue')->path
: redirect uri_for('/')->path;
};
}
ajax '/ajax/control/admin/delete' => require_role admin => sub {
send_error('Missing device', 400) unless param('device');
my $device = NetAddr::IP->new(param('device'));
send_error('Bad device', 400)
if ! $device or $device->addr eq '0.0.0.0';
return delete_device(
$device->addr, param('archive'), param('log'),
);
};
get '/admin/*' => require_role admin => sub {
my ($tag) = splat;
# trick the ajax into working as if this were a tabbed page
params->{tab} = $tag;
var(nav => 'admin');
template 'admintask', {
task => setting('_admin_tasks')->{ $tag },
};
};
true;

View File

@@ -0,0 +1,196 @@
package App::Netdisco::Web::Auth::Provider::DBIC;
use strict;
use warnings;
use base 'Dancer::Plugin::Auth::Extensible::Provider::Base';
# with thanks to yanick's patch at
# https://github.com/bigpresh/Dancer-Plugin-Auth-Extensible/pull/24
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Passphrase;
use Digest::MD5;
use Net::LDAP;
use Try::Tiny;
sub authenticate_user {
my ($self, $username, $password) = @_;
return unless defined $username;
my $user = $self->get_user_details($username) or return;
return unless $user->in_storage;
return $self->match_password($password, $user);
}
sub get_user_details {
my ($self, $username) = @_;
my $settings = $self->realm_settings;
my $database = schema($settings->{schema_name})
or die "No database connection";
my $users_table = $settings->{users_resultset} || 'User';
my $username_column = $settings->{users_username_column} || 'username';
my $user = try {
$database->resultset($users_table)->find({
$username_column => $username
});
};
# each of these settings permits no user in the database
# so create a pseudo user entry instead
if (not $user and not setting('validate_remote_user')
and (setting('trust_remote_user')
or setting('trust_x_remote_user')
or setting('no_auth'))) {
$user = $database->resultset($users_table)
->new_result({username => $username});
}
return $user;
}
sub get_user_roles {
my ($self, $username) = @_;
return unless defined $username;
my $settings = $self->realm_settings;
my $database = schema($settings->{schema_name})
or die "No database connection";
# Get details of the user first; both to check they exist, and so we have
# their ID to use.
my $user = $self->get_user_details($username)
or return;
my $roles = $settings->{roles_relationship} || 'roles';
my $role_column = $settings->{role_column} || 'role';
return [ try {
$user->$roles->get_column( $role_column )->all;
} ];
}
sub match_password {
my($self, $password, $user) = @_;
return unless $user;
my $settings = $self->realm_settings;
my $username_column = $settings->{users_username_column} || 'username';
return $user->ldap
? $self->match_with_ldap($password, $user->$username_column)
: $self->match_with_local_pass($password, $user);
}
sub match_with_local_pass {
my($self, $password, $user) = @_;
my $settings = $self->realm_settings;
my $password_column = $settings->{users_password_column} || 'password';
return unless $password and $user->$password_column;
if ($user->$password_column !~ m/^{[A-Z]+}/) {
my $sum = Digest::MD5::md5_hex($password);
if ($sum eq $user->$password_column) {
if (setting('safe_password_store')) {
# upgrade password if successful, and permitted
$user->update({password => passphrase($password)->generate});
}
return 1;
}
else {
return 0;
}
}
else {
return passphrase($password)->matches($user->$password_column);
}
}
sub match_with_ldap {
my($self, $pass, $user) = @_;
return unless setting('ldap') and ref {} eq ref setting('ldap');
my $conf = setting('ldap');
my $ldapuser = $conf->{user_string};
$ldapuser =~ s/\%USER\%?/$user/egi;
# If we can bind as anonymous or proxy user,
# search for user's distinguished name
if ($conf->{proxy_user}) {
my $user = $conf->{proxy_user};
my $pass = $conf->{proxy_pass};
my $attrs = ['distinguishedName'];
my $result = _ldap_search($ldapuser, $attrs, $user, $pass);
$ldapuser = $result->[0] if ($result->[0]);
}
# otherwise, if we can't search and aren't using AD and then construct DN
# by appending base
elsif ($ldapuser =~ m/=/) {
$ldapuser = "$ldapuser,$conf->{base}";
}
foreach my $server (@{$conf->{servers}}) {
my $opts = $conf->{opts} || {};
my $ldap = Net::LDAP->new($server, %$opts) or next;
my $msg = undef;
if ($conf->{tls_opts} ) {
$msg = $ldap->start_tls(%{$conf->{tls_opts}});
}
$msg = $ldap->bind($ldapuser, password => $pass);
$ldap->unbind(); # take down session
return 1 unless $msg->code();
}
return undef;
}
sub _ldap_search {
my ($filter, $attrs, $user, $pass) = @_;
my $conf = setting('ldap');
return undef unless defined($filter);
return undef if (defined $attrs and ref [] ne ref $attrs);
foreach my $server (@{$conf->{servers}}) {
my $opts = $conf->{opts} || {};
my $ldap = Net::LDAP->new($server, %$opts) or next;
my $msg = undef;
if ($conf->{tls_opts}) {
$msg = $ldap->start_tls(%{$conf->{tls_opts}});
}
if ( $user and $user ne 'anonymous' ) {
$msg = $ldap->bind($user, password => $pass);
}
else {
$msg = $ldap->bind();
}
$msg = $ldap->search(
base => $conf->{base},
filter => "($filter)",
attrs => $attrs,
);
$ldap->unbind(); # take down session
my $entries = [$msg->entries];
return $entries unless $msg->code();
}
return undef;
}
1;

View File

@@ -0,0 +1,112 @@
package App::Netdisco::Web::AuthN;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
hook 'before' => sub {
params->{return_url} ||= ((request->path ne uri_for('/')->path)
? request->uri : uri_for('/inventory')->path);
# from the internals of Dancer::Plugin::Auth::Extensible
my $provider = Dancer::Plugin::Auth::Extensible::auth_provider('users');
if (! session('logged_in_user') && request->path ne uri_for('/login')->path) {
if (setting('trust_x_remote_user')
and scalar request->header('X-REMOTE_USER')
and length scalar request->header('X-REMOTE_USER')) {
(my $user = scalar request->header('X-REMOTE_USER')) =~ s/@[^@]*$//;
return if setting('validate_remote_user')
and not $provider->get_user_details($user);
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
}
elsif (setting('trust_remote_user')
and defined $ENV{REMOTE_USER}
and length $ENV{REMOTE_USER}) {
(my $user = $ENV{REMOTE_USER}) =~ s/@[^@]*$//;
return if setting('validate_remote_user')
and not $provider->get_user_details($user);
session(logged_in_user => $user);
session(logged_in_user_realm => 'users');
}
elsif (setting('no_auth')) {
session(logged_in_user => 'guest');
session(logged_in_user_realm => 'users');
}
else {
# user has no AuthN - force to handler for '/'
request->path_info('/');
}
}
};
get qr{^/(?:login(?:/denied)?)?} => sub {
template 'index', { return_url => param('return_url') };
};
# override default login_handler so we can log access in the database
post '/login' => sub {
my $mode = (request->is_ajax ? 'API' : 'Web');
my ($success, $realm) = authenticate_user(
param('username'), param('password')
);
if ($success) {
session logged_in_user => param('username');
session logged_in_user_realm => $realm;
schema('netdisco')->resultset('UserLog')->create({
username => session('logged_in_user'),
userip => request->remote_address,
event => "Login ($mode)",
details => param('return_url'),
});
schema('netdisco')->resultset('User')
->find( session('logged_in_user') )
->update({ last_on => \'now()' });
return if request->is_ajax;
redirect param('return_url');
}
else {
session->destroy;
schema('netdisco')->resultset('UserLog')->create({
username => param('username'),
userip => request->remote_address,
event => "Login Failure ($mode)",
details => param('return_url'),
});
if (request->is_ajax) {
status('unauthorized');
}
else {
vars->{login_failed}++;
forward uri_for('/login'),
{ login_failed => 1, return_url => param('return_url') },
{ method => 'GET' };
}
}
};
# we override the default login_handler, so logout has to be handled as well
any ['get', 'post'] => '/logout' => sub {
schema('netdisco')->resultset('UserLog')->create({
username => session('logged_in_user'),
userip => request->remote_address,
event => "Logout",
details => '',
});
session->destroy;
redirect uri_for('/inventory')->path;
};
true;

View File

@@ -0,0 +1,186 @@
package App::Netdisco::Web::Device;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use URL::Encode 'url_params_mixed';
hook 'before' => sub {
my @default_port_columns_left = (
{ name => 'c_admin', label => 'Port Controls', default => '' },
{ name => 'c_port', label => 'Port', default => 'on' },
);
my @default_port_columns_right = (
{ name => 'c_descr', label => 'Description', default => '' },
{ name => 'c_comment', label => 'Last Comment', default => '' },
{ name => 'c_type', label => 'Type', default => '' },
{ name => 'c_duplex', label => 'Duplex', default => '' },
{ name => 'c_lastchange', label => 'Last Change', default => '' },
{ name => 'c_name', label => 'Name', default => 'on' },
{ name => 'c_speed', label => 'Speed', default => '' },
{ name => 'c_mac', label => 'Port MAC', default => '' },
{ name => 'c_mtu', label => 'MTU', default => '' },
{ name => 'c_pvid', label => 'Native VLAN', default => 'on' },
{ name => 'c_vmember', label => 'VLAN Membership', default => 'on' },
{ name => 'c_power', label => 'PoE', default => '' },
{ name => 'c_ssid', label => 'SSID', default => '' },
{ name => 'c_nodes', label => 'Connected Nodes', default => '' },
{ name => 'c_neighbors', label => 'Connected Devices', default => 'on' },
{ name => 'c_stp', label => 'Spanning Tree', default => '' },
{ name => 'c_up', label => 'Status', default => '' },
);
# build list of port detail columns
my @port_columns = ();
push @port_columns,
grep {$_->{position} eq 'left'} @{ setting('_extra_device_port_cols') };
push @port_columns, @default_port_columns_left;
push @port_columns,
grep {$_->{position} eq 'mid'} @{ setting('_extra_device_port_cols') };
push @port_columns, @default_port_columns_right;
push @port_columns,
grep {$_->{position} eq 'right'} @{ setting('_extra_device_port_cols') };
var('port_columns' => \@port_columns);
# view settings for port connected devices
var('connected_properties' => [
{ name => 'n_age', label => 'Age Stamp', default => '' },
{ name => 'n_ip4', label => 'IPv4 Addresses', default => 'on' },
{ name => 'n_ip6', label => 'IPv6 Addresses', default => 'on' },
{ name => 'n_netbios', label => 'NetBIOS', default => 'on' },
{ name => 'n_ssid', label => 'SSID', default => 'on' },
{ name => 'n_vendor', label => 'Vendor', default => '' },
{ name => 'n_archived', label => 'Archived Data', default => '' },
]);
return unless (request->path eq uri_for('/device')->path
or index(request->path, uri_for('/ajax/content/device')->path) == 0);
# override ports form defaults with cookie settings
my $cookie = (cookie('nd_ports-form') || '');
my $cdata = url_params_mixed($cookie);
if ($cdata and ref {} eq ref $cdata and not param('reset')) {
foreach my $item (@{ var('port_columns') }) {
my $key = $item->{name};
next unless defined $cdata->{$key}
and $cdata->{$key} =~ m/^[[:alnum:]_]+$/;
$item->{default} = $cdata->{$key};
}
foreach my $item (@{ var('connected_properties') }) {
my $key = $item->{name};
next unless defined $cdata->{$key}
and $cdata->{$key} =~ m/^[[:alnum:]_]+$/;
$item->{default} = $cdata->{$key};
}
foreach my $key (qw/age_num age_unit mac_format/) {
params->{$key} ||= $cdata->{$key}
if defined $cdata->{$key}
and $cdata->{$key} =~ m/^[[:alnum:]_]+$/;
}
}
# copy ports form defaults into request query params if this is
# a redirect from within the application (tab param is not set)
if (param('reset') or not param('tab') or param('tab') ne 'ports') {
foreach my $col (@{ var('port_columns') }) {
delete params->{$col->{name}};
params->{$col->{name}} = 'checked'
if $col->{default} eq 'on';
}
foreach my $col (@{ var('connected_properties') }) {
delete params->{$col->{name}};
params->{$col->{name}} = 'checked'
if $col->{default} eq 'on';
}
# not stored in the cookie
params->{'age_num'} ||= 3;
params->{'age_unit'} ||= 'months';
params->{'mac_format'} ||= 'IEEE';
if (param('reset')) {
params->{'age_num'} = 3;
params->{'age_unit'} = 'months';
params->{'mac_format'} = 'IEEE';
# nuke the port params cookie
cookie('nd_ports-form' => '', expires => '-1 day');
}
}
};
hook 'before_template' => sub {
my $tokens = shift;
# new searches will use these defaults in their sidebars
$tokens->{device_ports} = uri_for('/device', { tab => 'ports' });
# copy ports form defaults into helper values for building template links
foreach my $key (qw/age_num age_unit mac_format/) {
$tokens->{device_ports}->query_param($key, params->{$key});
}
$tokens->{mac_format_call} = 'as_'. lc(params->{'mac_format'})
if params->{'mac_format'};
foreach my $col (@{ var('port_columns') }) {
next unless $col->{default} eq 'on';
$tokens->{device_ports}->query_param($col->{name}, 'checked');
}
foreach my $col (@{ var('connected_properties') }) {
next unless $col->{default} eq 'on';
$tokens->{device_ports}->query_param($col->{name}, 'checked');
}
return unless (request->path eq uri_for('/device')->path
or index(request->path, uri_for('/ajax/content/device')->path) == 0);
# for templates to link to same page with modified query but same options
my $self_uri = uri_for(request->path, scalar params);
$self_uri->query_param_delete('q');
$self_uri->query_param_delete('f');
$self_uri->query_param_delete('prefer');
$tokens->{self_options} = $self_uri->query_form_hash;
};
get '/device' => require_login sub {
my $q = param('q');
my $devices = schema('netdisco')->resultset('Device');
# we are passed either dns or ip
my $dev = $devices->search({
-or => [
\[ 'host(me.ip) = ?' => [ bind_value => $q ] ],
'me.dns' => $q,
],
});
if ($dev->count == 0) {
return redirect uri_for('/', {nosuchdevice => 1, device => $q})->path_query;
}
# if passed dns, need to check for duplicates
# and use only ip for q param, if there are duplicates.
my $first = $dev->first;
my $others = ($devices->search({dns => $first->dns})->count() - 1);
params->{'tab'} ||= 'details';
template 'device', {
display_name => ($others ? $first->ip : ($first->dns || $first->ip)),
device => params->{'tab'},
};
};
true;

View File

@@ -0,0 +1,77 @@
package App::Netdisco::Web::GenericReport;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use Path::Class 'file';
use Safe;
use vars qw/$config @data/;
foreach my $report (@{setting('reports')}) {
my $r = $report->{tag};
register_report({
tag => $r,
label => $report->{label},
category => ($report->{category} || 'My Reports'),
($report->{hidden} ? (hidden => true) : ()),
provides_csv => true,
});
get "/ajax/content/report/$r" => require_login sub {
# TODO: this should be done by creating a new Virtual Result class on
# the fly (package...) and then calling DBIC register_class on it.
my $schema = ($report->{database} || 'netdisco');
my $rs = schema($schema)->resultset('Virtual::GenericReport')->result_source;
$rs->view_definition($report->{query});
$rs->remove_columns($rs->columns);
$rs->add_columns( exists $report->{query_columns}
? @{ $report->{query_columns} }
: (map {keys %{$_}} @{$report->{columns}})
);
my $set = schema($schema)->resultset('Virtual::GenericReport')
->search(undef, {
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
( (exists $report->{bind_params})
? (bind => [map { param($_) } @{ $report->{bind_params} }]) : () ),
});
@data = $set->all;
# Data Munging support...
my $compartment = Safe->new;
$config = $report; # closure for the config of this report
$compartment->share(qw/$config @data/);
$compartment->permit_only(qw/:default sort/);
my $munger = file(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'site_plugins', $r)->stringify;
my @results = ((-f $munger) ? $compartment->rdo( $munger ) : @data);
return if $@ or (0 == scalar @results);
if (request->is_ajax) {
template 'ajax/report/generic_report.tt',
{ results => \@results,
is_custom_report => true,
headings => [map {values %{$_}} @{$report->{columns}}],
columns => [map {keys %{$_}} @{$report->{columns}}] },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/generic_report_csv.tt',
{ results => \@results,
headings => [map {values %{$_}} @{$report->{columns}}],
columns => [map {keys %{$_}} @{$report->{columns}}] },
{ layout => undef };
}
};
}
true;

View File

@@ -0,0 +1,51 @@
package App::Netdisco::Web::Password;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Passphrase;
use Digest::MD5 ();
sub _make_password {
my $pass = (shift || passphrase->generate_random);
if (setting('safe_password_store')) {
return passphrase($pass)->generate;
}
else {
return Digest::MD5::md5_hex($pass),
}
}
sub _bail {
var('passchange_failed' => 1);
return template 'password.tt';
}
any ['get', 'post'] => '/password' => require_login sub {
my $old = param('old');
my $new = param('new');
my $confirm = param('confirm');
if (request->is_post) {
unless ($old and $new and $confirm and ($new eq $confirm)) {
return _bail();
}
my ($success, $realm) = authenticate_user(
session('logged_in_user'), $old
);
return _bail() if not $success;
my $user = schema('netdisco')->resultset('User')
->find({username => session('logged_in_user')});
return _bail() if not $user;
$user->update({password => _make_password($new)});
var('passchange_ok' => 1);
}
template 'password.tt';
};
true;

View File

@@ -0,0 +1,295 @@
package App::Netdisco::Web::Plugin;
use Dancer ':syntax';
use Dancer::Plugin;
use Path::Class 'dir';
set(
'_additional_css' => [],
'_additional_javascript' => [],
'_extra_device_port_cols' => [],
'_extra_device_details' => [],
'_navbar_items' => [],
'_search_tabs' => [],
'_device_tabs' => [],
'_admin_tasks' => {},
'_admin_order' => [],
'_reports_menu' => {},
'_reports' => {},
'_report_order' => [qw/Device Port IP Node VLAN Network Wireless/, 'My Reports'],
);
# this is what Dancer::Template::TemplateToolkit does by default
config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} ||= [ setting('views') ];
register 'register_template_path' => sub {
my ($self, $path) = plugin_args(@_);
if (!$path) {
return error "bad template path to register_template_paths";
}
push @{ config->{engines}->{netdisco_template_toolkit}->{INCLUDE_PATH} },
dir($path, 'views')->stringify;
};
sub _register_include {
my ($type, $plugin) = @_;
if (!$type) {
return error "bad type to _register_include";
}
if (!$plugin) {
return error "bad plugin name to register_$type";
}
push @{ setting("_additional_$type") }, $plugin;
}
register 'register_css' => sub {
my ($self, $plugin) = plugin_args(@_);
_register_include('css', $plugin);
};
register 'register_javascript' => sub {
my ($self, $plugin) = plugin_args(@_);
_register_include('javascript', $plugin);
};
register 'register_device_port_column' => sub {
my ($self, $config) = plugin_args(@_);
$config->{default} ||= '';
$config->{position} ||= 'right';
if (!$config->{name} or !$config->{label}) {
return error "bad config to register_device_port_column";
}
foreach my $item (@{ setting('_extra_device_port_cols') }) {
if ($item->{name} eq $config->{name}) {
$item = $config;
return;
}
}
push @{ setting('_extra_device_port_cols') }, $config;
};
register 'register_device_details' => sub {
my ($self, $config) = plugin_args(@_);
if (!$config->{name} or !$config->{label}) {
return error "bad config to register_device_details";
}
foreach my $item (@{ setting('_extra_device_details') }) {
if ($item->{name} eq $config->{name}) {
$item = $config;
return;
}
}
push @{ setting('_extra_device_details') }, $config;
};
register 'register_navbar_item' => sub {
my ($self, $config) = plugin_args(@_);
if (!$config->{tag}
or !$config->{path}
or !$config->{label}) {
return error "bad config to register_navbar_item";
}
foreach my $item (@{ setting('_navbar_items') }) {
if ($item->{tag} eq $config->{tag}) {
$item = $config;
return;
}
}
push @{ setting('_navbar_items') }, $config;
};
register 'register_admin_task' => sub {
my ($self, $config) = plugin_args(@_);
if (!$config->{tag}
or !$config->{label}) {
return error "bad config to register_admin_task";
}
push @{ setting('_admin_order') }, $config->{tag};
setting('_admin_tasks')->{ $config->{tag} } = $config;
};
sub _register_tab {
my ($nav, $config) = @_;
my $stash = setting("_${nav}_tabs");
if (!$config->{tag}
or !$config->{label}) {
return error "bad config to register_${nav}_item";
}
foreach my $item (@{ $stash }) {
if ($item->{tag} eq $config->{tag}) {
$item = $config;
return;
}
}
push @{ $stash }, $config;
}
register 'register_search_tab' => sub {
my ($self, $config) = plugin_args(@_);
_register_tab('search', $config);
};
register 'register_device_tab' => sub {
my ($self, $config) = plugin_args(@_);
_register_tab('device', $config);
};
register 'register_report' => sub {
my ($self, $config) = plugin_args(@_);
my @categories = @{ setting('_report_order') };
if (!$config->{category}
or !$config->{tag}
or !$config->{label}
or 0 == scalar grep {$config->{category} eq $_} @categories) {
return error "bad config to register_report";
}
if (0 == scalar grep {$_ eq $config->{tag}}
@{setting('_reports_menu')->{ $config->{category} }}) {
push @{setting('_reports_menu')->{ $config->{category} }}, $config->{tag};
}
foreach my $tag (@{setting('_reports_menu')->{ $config->{category} }}) {
if ($config->{tag} eq $tag) {
setting('_reports')->{$tag} = $config;
foreach my $rconfig (@{setting('reports')}) {
if ($rconfig->{tag} eq $tag) {
setting('_reports')->{$tag}->{'rconfig'} = $rconfig;
last;
}
}
}
}
};
register_plugin;
true;
=head1 NAME
App::Netdisco::Web::Plugin - Netdisco Web UI components
=head1 Introduction
L<App::Netdisco>'s plugin system allows you more control of what Netdisco
components are displayed in the web interface. Plugins can be distributed
independently from Netdisco and are a better alternative to source code
patches.
The following web interface components are implemented as plugins:
=over 4
=item *
Navigation Bar items (e.g. Inventory link)
=item *
Tabs for Search and Device pages
=item *
Reports (pre-canned searches)
=item *
Additional Device Port Columns
=item *
Additional Device Details
=item *
Admin Menu function (job control, manual topology, pseudo devices)
=back
This document explains how to configure which plugins are loaded. See
L<App::Netdisco::Manual::WritingPlugins> if you want to develop new plugins.
=head1 Application Configuration
Netdisco configuration supports a C<web_plugins> directive along with the
similar C<extra_web_plugins>. These list, in YAML format, the set of Perl
module names which are the plugins to be loaded. Each item injects one part of
the Netdisco web user interface.
You can override these settings to add, change, or remove entries from the
default lists. Here is an example of the C<web_plugins> list:
web_plugins:
- Inventory
- Report::DuplexMismatch
- Search::Device
- Search::Node
- Search::Port
- Device::Details
- Device::Ports
Any change should go into your local C<deployment.yml> configuration file. If
you want to view the default settings, see the C<share/config.yml> file in the
C<App::Netdisco> distribution.
=head1 How to Configure
The C<extra_web_plugins> setting is empty, and used only if you want to add
new plugins but not change the set enabled by default. If you do want to add
to or remove from the default set, then create a version of C<web_plugins>
instead.
Netdisco prepends "C<App::Netdisco::Web::Plugin::>" to any entry in the list.
For example, "C<Inventory>" will load the
C<App::Netdisco::Web::Plugin::Inventory> module.
Such plugin modules can either ship with the App::Netdisco distribution
itself, or be installed separately. Perl uses the standard C<@INC> path
searching mechanism to load the plugin modules.
If an entry in the list starts with a "C<+>" (plus) sign then Netdisco attemps
to load the module as-is, without prepending anything to the name. This allows
you to have App::Netdiso web UI plugins in other namespaces:
web_plugins:
- Inventory
- Search::Device
- Device::Details
- +My::Other::Netdisco::Web::Component
The order of the entries is significant. Unsurprisingly, the modules are
loaded in order. Therefore Navigation Bar items appear in the order listed,
and Tabs appear on the Search and Device pages in the order listed, and so on.
Finally, you can also prepend module names with "C<X::>", to support the
"Netdisco extension" namespace. For example, "C<X::Observium>" will load the
L<App::NetdiscoX::Web::Plugin::Observium> module.
=cut

View File

@@ -0,0 +1,32 @@
package App::Netdisco::Web::Plugin::AdminTask::JobQueue;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::JobQueue qw/jq_log jq_delete/;
register_admin_task({
tag => 'jobqueue',
label => 'Job Queue',
});
ajax '/ajax/control/admin/jobqueue/del' => require_role admin => sub {
send_error('Missing job', 400) unless param('job');
jq_delete( param('job') );
};
ajax '/ajax/control/admin/jobqueue/delall' => require_role admin => sub {
jq_delete();
};
ajax '/ajax/content/admin/jobqueue' => require_role admin => sub {
content_type('text/html');
template 'ajax/admintask/jobqueue.tt', {
results => [ jq_log ],
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,75 @@
package App::Netdisco::Web::Plugin::AdminTask::NodeMonitor;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Node 'check_mac';
register_admin_task({
tag => 'nodemonitor',
label => 'Node Monitor',
});
sub _sanity_ok {
return 0 unless param('mac')
and check_mac(undef, param('mac'));
params->{mac} = check_mac(undef, param('mac'));
return 1;
}
ajax '/ajax/control/admin/nodemonitor/add' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $monitor = schema('netdisco')->resultset('NodeMonitor')
->create({
mac => param('mac'),
active => (param('active') ? \'true' : \'false'),
why => param('why'),
cc => param('cc'),
});
});
};
ajax '/ajax/control/admin/nodemonitor/del' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
schema('netdisco')->resultset('NodeMonitor')
->find({mac => param('mac')})->delete;
});
};
ajax '/ajax/control/admin/nodemonitor/update' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $monitor = schema('netdisco')->resultset('NodeMonitor')
->find({mac => param('mac')});
return unless $monitor;
$monitor->update({
mac => param('mac'),
active => (param('active') ? \'true' : \'false'),
why => param('why'),
cc => param('cc'),
date => \'now()',
});
});
};
ajax '/ajax/content/admin/nodemonitor' => require_role admin => sub {
my $set = schema('netdisco')->resultset('NodeMonitor')
->search(undef, { order_by => [qw/active date mac/] });
content_type('text/html');
template 'ajax/admintask/nodemonitor.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,82 @@
package App::Netdisco::Web::Plugin::AdminTask::OrphanedDevices;
use strict;
use warnings;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_admin_task(
{ tag => 'orphaned',
label => 'Orphaned Devices / Networks',
provides_csv => 1,
}
);
get '/ajax/content/admin/orphaned' => require_role admin => sub {
my @tree = schema('netdisco')->resultset('Virtual::UnDirEdgesAgg')
->search( undef, { prefetch => 'device' } )->hri->all;
my @orphans
= schema('netdisco')->resultset('Virtual::OrphanedDevices')->search()
->order_by('ip')->hri->all;
return unless ( scalar @tree || scalar @orphans );
my @ordered;
if ( scalar @tree ) {
my %tree = map { $_->{'left_ip'} => $_ } @tree;
my $current_graph = 0;
my %visited = ();
my @to_visit = ();
foreach my $node ( keys %tree ) {
next if exists $visited{$node};
$current_graph++;
@to_visit = ($node);
while (@to_visit) {
my $node_to_visit = shift @to_visit;
$visited{$node_to_visit} = $current_graph;
push @to_visit,
grep { !exists $visited{$_} }
@{ $tree{$node_to_visit}->{'links'} };
}
}
my @graphs = ();
foreach my $key ( keys %visited ) {
push @{ $graphs[ $visited{$key} - 1 ] }, $tree{$key}->{'device'};
}
@ordered = sort { scalar @{$b} <=> scalar @{$a} } @graphs;
}
return if ( scalar @ordered < 2 && !scalar @tree );
if ( request->is_ajax ) {
template 'ajax/admintask/orphaned.tt',
{
orphans => \@orphans,
graphs => \@ordered,
},
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/admintask/orphaned_csv.tt',
{
orphans => \@orphans,
graphs => \@ordered,
},
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,24 @@
package App::Netdisco::Web::Plugin::AdminTask::PollerPerformance;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_admin_task({
tag => 'performance',
label => 'Poller Performance',
});
ajax '/ajax/content/admin/performance' => require_role admin => sub {
my $set = schema('netdisco')->resultset('Virtual::PollerPerformance');
content_type('text/html');
template 'ajax/admintask/performance.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,100 @@
package App::Netdisco::Web::Plugin::AdminTask::PseudoDevice;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use NetAddr::IP::Lite ':lower';
register_admin_task({
tag => 'pseudodevice',
label => 'Pseudo Devices',
});
sub _sanity_ok {
return 0 unless param('dns')
and param('dns') =~ m/^[[:print:]]+$/
and param('dns') !~ m/[[:space:]]/;
my $ip = NetAddr::IP::Lite->new(param('ip'));
return 0 unless ($ip and $ip->addr ne '0.0.0.0');
return 0 unless param('ports')
and param('ports') =~ m/^[[:digit:]]+$/;
return 1;
}
ajax '/ajax/control/admin/pseudodevice/add' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $device = schema('netdisco')->resultset('Device')
->create({
ip => param('ip'),
dns => param('dns'),
vendor => 'netdisco',
last_discover => \'now()',
});
return unless $device;
$device->ports->populate([
[qw/port type/],
map {["Port$_", 'other']} @{[1 .. param('ports')]},
]);
# device_ip table is used to show whether topo is "broken"
schema('netdisco')->resultset('DeviceIp')
->create({
ip => param('ip'),
alias => param('ip'),
});
});
};
ajax '/ajax/control/admin/pseudodevice/del' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
forward '/ajax/control/admin/delete', { device => param('ip') };
};
ajax '/ajax/control/admin/pseudodevice/update' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $device = schema('netdisco')->resultset('Device')
->with_port_count->find({ip => param('ip')});
return unless $device;
my $count = $device->port_count;
if (param('ports') > $count) {
my $start = $count + 1;
$device->ports->populate([
[qw/port type/],
map {["Port$_", 'other']} @{[$start .. param('ports')]},
]);
}
elsif (param('ports') < $count) {
my $start = param('ports') + 1;
$device->ports
->single({port => "Port$_"})->delete
for ($start .. $count);
}
});
};
ajax '/ajax/content/admin/pseudodevice' => require_role admin => sub {
my $set = schema('netdisco')->resultset('Device')
->search(
{vendor => 'netdisco'},
{order_by => { -desc => 'last_discover' }},
)->with_port_count;
content_type('text/html');
template 'ajax/admintask/pseudodevice.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,24 @@
package App::Netdisco::Web::Plugin::AdminTask::SlowDevices;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_admin_task({
tag => 'slowdevices',
label => 'Slowest Devices',
});
ajax '/ajax/content/admin/slowdevices' => require_role admin => sub {
my $set = schema('netdisco')->resultset('Virtual::SlowDevices');
content_type('text/html');
template 'ajax/admintask/slowdevices.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,142 @@
package App::Netdisco::Web::Plugin::AdminTask::Topology;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Device 'get_device';
use Try::Tiny;
use NetAddr::IP::Lite ':lower';
register_admin_task({
tag => 'topology',
label => 'Manual Device Topology',
});
sub _sanity_ok {
my $dev1 = NetAddr::IP::Lite->new(param('dev1'));
return 0 unless ($dev1 and $dev1->addr ne '0.0.0.0');
my $dev2 = NetAddr::IP::Lite->new(param('dev2'));
return 0 unless ($dev2 and $dev2->addr ne '0.0.0.0');
return 0 unless param('port1');
return 0 unless param('port2');
return 1;
}
ajax '/ajax/control/admin/topology/add' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
my $device = schema('netdisco')->resultset('Topology')
->create({
dev1 => param('dev1'),
port1 => param('port1'),
dev2 => param('dev2'),
port2 => param('port2'),
});
# re-set remote device details in affected ports
# could fail for bad device or port names
try {
schema('netdisco')->txn_do(sub {
# only work on root_ips
my $left = get_device(param('dev1'));
my $right = get_device(param('dev2'));
# skip bad entries
return unless ($left->in_storage and $right->in_storage);
$left->ports
->search({port => param('port1')}, {for => 'update'})
->single()
->update({
remote_ip => param('dev2'),
remote_port => param('port2'),
remote_type => undef,
remote_id => undef,
is_uplink => \"true",
manual_topo => \"true",
});
$right->ports
->search({port => param('port2')}, {for => 'update'})
->single()
->update({
remote_ip => param('dev1'),
remote_port => param('port1'),
remote_type => undef,
remote_id => undef,
is_uplink => \"true",
manual_topo => \"true",
});
});
};
};
ajax '/ajax/control/admin/topology/del' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $device = schema('netdisco')->resultset('Topology')
->search({
dev1 => param('dev1'),
port1 => param('port1'),
dev2 => param('dev2'),
port2 => param('port2'),
})->delete;
});
# re-set remote device details in affected ports
# could fail for bad device or port names
try {
schema('netdisco')->txn_do(sub {
# only work on root_ips
my $left = get_device(param('dev1'));
my $right = get_device(param('dev2'));
# skip bad entries
return unless ($left->in_storage and $right->in_storage);
$left->ports
->search({port => param('port1')}, {for => 'update'})
->single()
->update({
remote_ip => undef,
remote_port => undef,
remote_type => undef,
remote_id => undef,
is_uplink => \"false",
manual_topo => \"false",
});
$right->ports
->search({port => param('port2')}, {for => 'update'})
->single()
->update({
remote_ip => undef,
remote_port => undef,
remote_type => undef,
remote_id => undef,
is_uplink => \"false",
manual_topo => \"false",
});
});
};
};
ajax '/ajax/content/admin/topology' => require_role admin => sub {
my $set = schema('netdisco')->resultset('Topology')
->search({},{order_by => [qw/dev1 dev2 port1/]});
content_type('text/html');
template 'ajax/admintask/topology.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,48 @@
package App::Netdisco::Web::Plugin::AdminTask::UndiscoveredNeighbors;
use strict;
use warnings;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::Device qw/is_discoverable/;
use App::Netdisco::Web::Plugin;
register_admin_task(
{ tag => 'undiscoveredneighbors',
label => 'Undiscovered Neighbors',
provides_csv => 1,
}
);
get '/ajax/content/admin/undiscoveredneighbors' => require_role admin => sub {
my @results
= schema('netdisco')->resultset('Virtual::UndiscoveredNeighbors')
->order_by('ip')->hri->all;
return unless scalar @results;
# Don't include devices excluded from discovery by config
# but only if the number of devices is small, as it triggers a
# SELECT per device to check.
if (scalar @results < 50) {
@results
= grep { is_discoverable( $_->{'remote_ip'}, $_->{'remote_type'} ) }
@results;
}
return unless scalar @results;
if ( request->is_ajax ) {
template 'ajax/admintask/undiscoveredneighbors.tt',
{ results => \@results, },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/admintask/undiscoveredneighbors_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,66 @@
package App::Netdisco::Web::Plugin::AdminTask::UserLog;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::ExpandParams 'expand_hash';
use App::Netdisco::Web::Plugin;
register_admin_task(
{ tag => 'userlog',
label => 'User Activity Log',
}
);
ajax '/ajax/control/admin/userlog/data' => require_role admin => sub {
send_error( 'Missing parameter', 400 )
unless ( param('draw') && param('draw') =~ /\d+/ );
my $rs = schema('netdisco')->resultset('UserLog');
my $exp_params = expand_hash( scalar params );
my $recordsTotal = $rs->count;
my @data = $rs->get_datatables_data($exp_params)->hri->all;
my $recordsFiltered = $rs->get_datatables_filtered_count($exp_params);
content_type 'application/json';
return to_json(
{ draw => int( param('draw') ),
recordsTotal => int($recordsTotal),
recordsFiltered => int($recordsFiltered),
data => \@data,
}
);
};
ajax '/ajax/control/admin/userlog/del' => require_role admin => sub {
send_error( 'Missing entry', 400 ) unless param('entry');
schema('netdisco')->txn_do(
sub {
my $device = schema('netdisco')->resultset('UserLog')
->search( { entry => param('entry') } )->delete;
}
);
};
ajax '/ajax/control/admin/userlog/delall' => require_role admin => sub {
schema('netdisco')->txn_do(
sub {
my $device = schema('netdisco')->resultset('UserLog')->delete;
}
);
};
ajax '/ajax/content/admin/userlog' => require_role admin => sub {
content_type('text/html');
template 'ajax/admintask/userlog.tt', {}, { layout => undef };
};
1;

View File

@@ -0,0 +1,106 @@
package App::Netdisco::Web::Plugin::AdminTask::Users;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Passphrase;
use App::Netdisco::Web::Plugin;
use Digest::MD5 ();
register_admin_task({
tag => 'users',
label => 'User Management',
provides_csv => 1,
});
sub _sanity_ok {
return 0 unless param('username')
and param('username') =~ m/^[[:print:] ]+$/;
return 1;
}
sub _make_password {
my $pass = (shift || passphrase->generate_random);
if (setting('safe_password_store')) {
return passphrase($pass)->generate;
}
else {
return Digest::MD5::md5_hex($pass),
}
}
ajax '/ajax/control/admin/users/add' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $user = schema('netdisco')->resultset('User')
->create({
username => param('username'),
password => _make_password(param('password')),
fullname => param('fullname'),
ldap => (param('ldap') ? \'true' : \'false'),
port_control => (param('port_control') ? \'true' : \'false'),
admin => (param('admin') ? \'true' : \'false'),
note => param('note'),
});
});
};
ajax '/ajax/control/admin/users/del' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
schema('netdisco')->resultset('User')
->find({username => param('username')})->delete;
});
};
ajax '/ajax/control/admin/users/update' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $user = schema('netdisco')->resultset('User')
->find({username => param('username')});
return unless $user;
$user->update({
((param('password') ne '********')
? (password => _make_password(param('password')))
: ()),
fullname => param('fullname'),
ldap => (param('ldap') ? \'true' : \'false'),
port_control => (param('port_control') ? \'true' : \'false'),
admin => (param('admin') ? \'true' : \'false'),
note => param('note'),
});
});
};
get '/ajax/content/admin/users' => require_role admin => sub {
my @results = schema('netdisco')->resultset('User')
->search(undef, {
'+columns' => {
created => \"to_char(creation, 'YYYY-MM-DD HH24:MI')",
last_seen => \"to_char(last_on, 'YYYY-MM-DD HH24:MI')",
},
order_by => [qw/fullname username/]
})->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
template 'ajax/admintask/users.tt',
{ results => \@results, },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/admintask/users_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
true;

View File

@@ -0,0 +1,35 @@
package App::Netdisco::Web::Plugin::Device::Addresses;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_device_tab( { tag => 'addresses', label => 'Addresses', provides_csv => 1 } );
# device interface addresses
get '/ajax/content/device/addresses' => require_login sub {
my $q = param('q');
my $device
= schema('netdisco')->resultset('Device')->search_for_device($q)
or send_error( 'Bad device', 400 );
my @results = $device->device_ips->search( {}, { order_by => 'alias' } )->hri->all;
return unless scalar @results;
if (request->is_ajax) {
my $json = to_json( \@results );
template 'ajax/device/addresses.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/device/addresses_csv.tt', { results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,33 @@
package App::Netdisco::Web::Plugin::Device::Details;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_device_tab({ tag => 'details', label => 'Details' });
# device details table
ajax '/ajax/content/device/details' => require_login sub {
my $q = param('q');
my $device = schema('netdisco')->resultset('Device')
->search_for_device($q) or send_error('Bad device', 400);
my @results
= schema('netdisco')->resultset('Device')
->search( { 'me.ip' => $device->ip } )->with_times()
->hri->all;
my @power
= schema('netdisco')->resultset('DevicePower')
->search( { 'me.ip' => $device->ip } )->with_poestats->hri->all;
content_type('text/html');
template 'ajax/device/details.tt', {
d => $results[0], p => \@power
}, { layout => undef };
};
1;

View File

@@ -0,0 +1,30 @@
package App::Netdisco::Web::Plugin::Device::Modules;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::Web (); # for sort_module
use App::Netdisco::Web::Plugin;
register_device_tab({ tag => 'modules', label => 'Modules' });
ajax '/ajax/content/device/modules' => require_login sub {
my $q = param('q');
my $device = schema('netdisco')->resultset('Device')
->search_for_device($q) or send_error('Bad device', 400);
my @set = $device->modules->search({}, {order_by => { -asc => [qw/parent class pos index/] }});
# sort modules (empty set would be a 'no records' msg)
my $results = &App::Netdisco::Util::Web::sort_modules( \@set );
return unless scalar %$results;
content_type('text/html');
template 'ajax/device/modules.tt', {
nodes => $results,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,129 @@
package App::Netdisco::Web::Plugin::Device::Neighbors;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_device_tab({ tag => 'netmap', label => 'Neighbors' });
ajax '/ajax/content/device/netmap' => require_login sub {
content_type('text/html');
template 'ajax/device/netmap.tt', {}, { layout => undef };
};
sub _get_name {
my $ip = shift;
my $domain = quotemeta( setting('domain_suffix') || '' );
(my $dns = (var('devices')->{$ip} || '')) =~ s/$domain$//;
return ($dns || $ip);
}
sub _add_children {
my ($ptr, $childs, $step, $limit) = @_;
return $step if $limit and $step > $limit;
my @legit = ();
my $max = $step;
foreach my $c (@$childs) {
next if exists var('seen')->{$c};
var('seen')->{$c}++;
push @legit, $c;
push @{$ptr}, {
name => _get_name($c),
fullname => (var('devices')->{$c} || $c),
ip => $c,
};
}
for (my $i = 0; $i < @legit; $i++) {
$ptr->[$i]->{children} = [];
my $nm = _add_children($ptr->[$i]->{children}, var('links')->{$legit[$i]},
($step + 1), $limit);
$max = $nm if $nm > $max;
}
return $max;
}
# d3 seems not to use proper ajax semantics, so get instead of ajax
get '/ajax/data/device/netmap' => require_login sub {
my $q = param('q');
my $vlan = param('vlan');
undef $vlan if (defined $vlan and $vlan !~ m/^\d+$/);
my $depth = (param('depth') || 8);
undef $depth if (defined $depth and $depth !~ m/^\d+$/);
my $device = schema('netdisco')->resultset('Device')
->search_for_device($q) or send_error('Bad device', 400);
my $start = $device->ip;
my @devices = schema('netdisco')->resultset('Device')->search({}, {
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
columns => ['ip', 'dns'],
})->all;
var(devices => { map { $_->{ip} => $_->{dns} } @devices });
var(links => {});
my $rs = schema('netdisco')->resultset('Virtual::DeviceLinks')->search({}, {
columns => [qw/left_ip right_ip/],
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
});
if ($vlan) {
$rs = $rs->search({
'left_vlans.vlan' => $vlan,
'right_vlans.vlan' => $vlan,
}, {
join => [qw/left_vlans right_vlans/],
});
}
while (my $l = $rs->next) {
var('links')->{ $l->{left_ip} } ||= [];
push @{ var('links')->{ $l->{left_ip} } }, $l->{right_ip};
}
my %tree = (
ip => $start,
name => _get_name($start),
fullname => (var('devices')->{$start} || $start),
children => [],
);
var(seen => {$start => 1});
my $max = _add_children($tree{children}, var('links')->{$start}, 1, $depth);
$tree{scale} = $max;
content_type('application/json');
to_json(\%tree);
};
ajax '/ajax/data/device/alldevicelinks' => require_login sub {
my @devices = schema('netdisco')->resultset('Device')->search({}, {
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
columns => ['ip', 'dns'],
})->all;
var(devices => { map { $_->{ip} => $_->{dns} } @devices });
my $rs = schema('netdisco')->resultset('Virtual::DeviceLinks')->search({}, {
result_class => 'DBIx::Class::ResultClass::HashRefInflator',
});
my %tree = ();
while (my $l = $rs->next) {
push @{ $tree{ _get_name($l->{left_ip} )} },
_get_name($l->{right_ip});
}
content_type('application/json');
to_json(\%tree);
};
true;

View File

@@ -0,0 +1,192 @@
package App::Netdisco::Web::Plugin::Device::Ports;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::Web (); # for sort_port
use App::Netdisco::Web::Plugin;
register_device_tab({ tag => 'ports', label => 'Ports', provides_csv => 1 });
# device ports with a description (er, name) matching
get '/ajax/content/device/ports' => require_login sub {
my $q = param('q');
my $prefer = param('prefer');
$prefer = ''
unless defined $prefer and $prefer =~ m/^(?:port|name|vlan)$/;
my $device = schema('netdisco')->resultset('Device')
->search_for_device($q) or send_error('Bad device', 400);
my $set = $device->ports;
# refine by ports if requested
my $f = param('f');
if ($f) {
if (($prefer eq 'vlan') or not $prefer and $f =~ m/^\d+$/) {
if (param('invert')) {
$set = $set->search({
'me.vlan' => { '!=' => $f },
'port_vlans.vlan' => [
'-or' => { '!=' => $f }, { '=' => undef }
],
}, { join => 'port_vlans' });
}
else {
$set = $set->search({
-or => {
'me.vlan' => $f,
'port_vlans.vlan' => $f,
},
}, { join => 'port_vlans' });
}
return unless $set->count;
}
else {
if (param('partial')) {
# change wildcard chars to SQL
$f =~ s/\*/%/g;
$f =~ s/\?/_/g;
# set wilcards at param boundaries
if ($f !~ m/[%_]/) {
$f =~ s/^\%*/%/;
$f =~ s/\%*$/%/;
}
# enable ILIKE op
$f = { (param('invert') ? '-not_ilike' : '-ilike') => $f };
}
elsif (param('invert')) {
$f = { '!=' => $f };
}
if (($prefer eq 'port') or not $prefer and
$set->search({'me.port' => $f})->count) {
$set = $set->search({
-or => [
'me.port' => $f,
'me.slave_of' => $f,
],
});
}
else {
$set = $set->search({'me.name' => $f});
return unless $set->count;
}
}
}
# filter for port status if asked
my %port_state = map {$_ => 1}
(ref [] eq ref param('port_state') ? @{param('port_state')}
: param('port_state') ? param('port_state') : ());
return unless scalar keys %port_state;
if (exists $port_state{free}) {
if (scalar keys %port_state == 1) {
$set = $set->only_free_ports({
age_num => (param('age_num') || 3),
age_unit => (param('age_unit') || 'months')
});
}
else {
$set = $set->with_is_free({
age_num => (param('age_num') || 3),
age_unit => (param('age_unit') || 'months')
});
}
delete $port_state{free};
}
if (scalar keys %port_state < 3) {
my @combi = ();
push @combi, {'me.up' => 'up'}
if exists $port_state{up};
push @combi, {'me.up_admin' => 'up', 'me.up' => { '!=' => 'up'}}
if exists $port_state{down};
push @combi, {'me.up_admin' => { '!=' => 'up'}}
if exists $port_state{shut};
$set = $set->search({-or => \@combi});
}
# get aggregate master status
$set = $set->search({}, {
'join' => 'agg_master',
'+select' => [qw/agg_master.up_admin agg_master.up/],
'+as' => [qw/agg_master_up_admin agg_master_up/],
});
# make sure query asks for formatted timestamps when needed
$set = $set->with_times if param('c_lastchange');
# get vlans on the port, if there aren't too many
my $port_cnt = $device->ports->count() || 1;
my $vlan_cnt = $device->port_vlans->count() || 1;
my $vmember_ok =
(($vlan_cnt / $port_cnt) <= setting('devport_vlan_limit'));
if ($vmember_ok) {
$set = $set->search_rs({}, { prefetch => 'all_port_vlans' })->with_vlan_count
if param('c_vmember');
}
# what kind of nodes are we interested in?
my $nodes_name = (param('n_archived') ? 'nodes' : 'active_nodes');
$nodes_name .= '_with_age' if param('n_age');
if (param('c_nodes')) {
my $ips = ((param('n_ip4') and param('n_ip6')) ? 'ips'
: param('n_ip4') ? 'ip4s'
: 'ip6s');
# retrieve active/all connected nodes, if asked for
$set = $set->search_rs({}, { prefetch => [{$nodes_name => $ips}] });
$set = $set->search_rs({}, { order_by => ["${nodes_name}.vlan", "${nodes_name}.mac", "${ips}.ip"] });
# retrieve wireless SSIDs, if asked for
$set = $set->search_rs({}, { prefetch => [{$nodes_name => 'wireless'}] })
if param('n_ssid');
# retrieve NetBIOS, if asked for
$set = $set->search_rs({}, { prefetch => [{$nodes_name => 'netbios'}] })
if param('n_netbios');
# retrieve vendor, if asked for
$set = $set->search_rs({}, { prefetch => [{$nodes_name => 'oui'}] })
if param('n_vendor');
}
# retrieve SSID, if asked for
$set = $set->search({}, { prefetch => 'ssid' }) if param('c_ssid');
# retrieve neighbor devices, if asked for
$set = $set->search_rs({}, { prefetch => [{neighbor_alias => 'device'}] })
if param('c_neighbors');
# sort ports (empty set would be a 'no records' msg)
my $results = [ sort { &App::Netdisco::Util::Web::sort_port($a->port, $b->port) } $set->all ];
return unless scalar @$results;
if (request->is_ajax) {
template 'ajax/device/ports.tt', {
results => $results,
nodes => $nodes_name,
device => $device,
vmember_ok => $vmember_ok,
}, { layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/device/ports_csv.tt', {
results => $results,
nodes => $nodes_name,
device => $device,
}, { layout => undef };
}
};
true;

View File

@@ -0,0 +1,27 @@
package App::Netdisco::Web::Plugin::Inventory;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_navbar_item({
tag => 'inventory',
path => '/inventory',
label => 'Inventory',
});
get '/inventory' => require_login sub {
my $models = schema('netdisco')->resultset('Device')->get_models();
my $releases = schema('netdisco')->resultset('Device')->get_releases();
var(nav => 'inventory');
template 'inventory', {
models => $models,
releases => $releases,
};
};
true;

View File

@@ -0,0 +1,41 @@
package App::Netdisco::Web::Plugin::Report::ApChannelDist;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Wireless',
tag => 'apchanneldist',
label => 'Access Point Channel Distribution',
provides_csv => 1,
}
);
get '/ajax/content/report/apchanneldist' => require_login sub {
my @results = schema('netdisco')->resultset('DevicePortWireless')->search(
{ channel => { '!=', '0' } },
{ select => [ 'channel', { count => 'channel' } ],
as => [qw/ channel ch_count /],
group_by => [qw/channel/],
order_by => { -desc => [qw/count/] },
},
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/apchanneldist.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/apchanneldist_csv.tt', { results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,51 @@
package App::Netdisco::Web::Plugin::Report::ApClients;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Wireless',
tag => 'apclients',
label => 'Access Point Client Count',
provides_csv => 1,
}
);
get '/ajax/content/report/apclients' => require_login sub {
my @results = schema('netdisco')->resultset('Device')->search(
{ 'nodes.time_last' => { '>=', \'me.last_macsuck' } },
{ select => [ 'ip', 'dns', 'name', 'model', 'location' ],
join => { 'ports' => { 'ssid' => 'nodes' } },
'+columns' => [
{ 'port' => 'ports.port' },
{ 'description' => 'ports.name' },
{ 'ssid' => 'ssid.ssid' },
{ 'mac_count' => { count => 'nodes.mac' } },
],
group_by => [
'me.ip', 'me.dns', 'me.name', 'me.model',
'me.location', 'ports.port', 'ports.descr', 'ports.name', 'ssid.ssid',
],
order_by => { -desc => [qw/count/] },
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/apclients.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/apclients_csv.tt',
{ results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,58 @@
package App::Netdisco::Web::Plugin::Report::ApRadioChannelPower;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::ExpandParams 'expand_hash';
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Wireless',
tag => 'apradiochannelpower',
label => 'Access Point Radios Channel and Power',
provides_csv => 1,
}
);
get '/ajax/content/report/apradiochannelpower/data' => require_login sub {
send_error( 'Missing parameter', 400 )
unless ( param('draw') && param('draw') =~ /\d+/ );
my $rs = schema('netdisco')->resultset('Virtual::ApRadioChannelPower');
my $exp_params = expand_hash( scalar params );
my $recordsTotal = $rs->count;
my @data = $rs->get_datatables_data($exp_params)->hri->all;
my $recordsFiltered = $rs->get_datatables_filtered_count($exp_params);
content_type 'application/json';
return to_json(
{ draw => int( param('draw') ),
recordsTotal => int($recordsTotal),
recordsFiltered => int($recordsFiltered),
data => \@data,
}
);
};
get '/ajax/content/report/apradiochannelpower' => require_login sub {
if ( request->is_ajax ) {
template 'ajax/report/apradiochannelpower.tt', {},
{ layout => undef };
}
else {
my @results
= schema('netdisco')->resultset('Virtual::ApRadioChannelPower')
->hri->all;
return unless scalar @results;
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/apradiochannelpower_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,42 @@
package App::Netdisco::Web::Plugin::Report::DeviceAddrNoDNS;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Device',
tag => 'deviceaddrnodns',
label => 'Addresses without DNS Entries',
provides_csv => 1,
}
);
get '/ajax/content/report/deviceaddrnodns' => require_login sub {
my @results = schema('netdisco')->resultset('Device')->search(
{ 'device_ips.dns' => undef },
{ select => [ 'ip', 'dns', 'name', 'location', 'contact' ],
join => [qw/device_ips/],
'+columns' => [ { 'alias' => 'device_ips.alias' }, ],
order_by => { -asc => [qw/me.ip device_ips.alias/] },
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json (\@results);
template 'ajax/report/deviceaddrnodns.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/deviceaddrnodns_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,38 @@
package App::Netdisco::Web::Plugin::Report::DeviceByLocation;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Device',
tag => 'devicebylocation',
label => 'By Location',
provides_csv => 1,
}
);
get '/ajax/content/report/devicebylocation' => require_login sub {
my @results
= schema('netdisco')->resultset('Device')
->columns( [qw/ ip dns name location vendor model /] )
->order_by( [qw/ location name ip vendor model /] )->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/devicebylocation.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/devicebylocation_csv.tt',
{ results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,41 @@
package App::Netdisco::Web::Plugin::Report::DeviceDnsMismatch;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Device',
tag => 'devicednsmismatch',
label => 'Device Name / DNS Mismatches',
provides_csv => 1,
}
);
get '/ajax/content/report/devicednsmismatch' => require_login sub {
my $suffix = setting('domain_suffix') || '';
my @results
= schema('netdisco')->resultset('Virtual::DeviceDnsMismatch')
->search( undef, { bind => [ $suffix, $suffix ] } )
->columns( [qw/ ip dns name location contact /] )->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/devicednsmismatch.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/devicednsmismatch_csv.tt',
{ results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,58 @@
package App::Netdisco::Web::Plugin::Report::DevicePoeStatus;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::ExpandParams 'expand_hash';
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Device',
tag => 'devicepoestatus',
label => 'Power over Ethernet (PoE) Status',
provides_csv => 1,
}
);
get '/ajax/content/report/devicepoestatus/data' => require_login sub {
send_error( 'Missing parameter', 400 )
unless ( param('draw') && param('draw') =~ /\d+/ );
my $rs = schema('netdisco')->resultset('Virtual::DevicePoeStatus');
my $exp_params = expand_hash( scalar params );
my $recordsTotal = $rs->count;
my @data = $rs->get_datatables_data($exp_params)->hri->all;
my $recordsFiltered = $rs->get_datatables_filtered_count($exp_params);
content_type 'application/json';
return to_json(
{ draw => int( param('draw') ),
recordsTotal => int($recordsTotal),
recordsFiltered => int($recordsFiltered),
data => \@data,
}
);
};
get '/ajax/content/report/devicepoestatus' => require_login sub {
if ( request->is_ajax ) {
template 'ajax/report/devicepoestatus.tt', {}, { layout => undef };
}
else {
my @results
= schema('netdisco')->resultset('Virtual::DevicePoeStatus')
->hri->all;
return unless scalar @results;
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/devicepoestatus_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,36 @@
package App::Netdisco::Web::Plugin::Report::DuplexMismatch;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Port',
tag => 'duplexmismatch',
label => 'Duplex Mismatches Between Devices',
provides_csv => 1,
}
);
get '/ajax/content/report/duplexmismatch' => require_login sub {
my @results
= schema('netdisco')->resultset('Virtual::DuplexMismatch')->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/duplexmismatch.tt', { results => $json, },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/duplexmismatch_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,44 @@
package App::Netdisco::Web::Plugin::Report::HalfDuplex;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Port',
tag => 'halfduplex',
label => 'Ports in Half Duplex Mode',
provides_csv => 1,
}
);
get '/ajax/content/report/halfduplex' => require_login sub {
my $format = param('format');
my @results
= schema('netdisco')->resultset('DevicePort')
->columns( [qw/ ip port name duplex /] )->search(
{ up => 'up', duplex => { '-ilike' => 'half' } },
{ '+columns' => [qw/ device.dns device.name /],
join => [qw/ device /],
collapse => 1,
}
)->order_by( [qw/ device.dns port /] )->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/halfduplex.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/halfduplex_csv.tt',
{ results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,30 @@
package App::Netdisco::Web::Plugin::Report::InventoryByModelByOS;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Device',
tag => 'inventorybymodelbyos',
label => 'Inventory by Model by OS',
provides_csv => 0,
}
);
get '/ajax/content/report/inventorybymodelbyos' => require_login sub {
my @results = schema('netdisco')->resultset('Device')->search(undef, {
columns => [qw/vendor model os os_ver/],
select => [ { count => 'os_ver' } ],
as => [qw/ os_ver_count /],
group_by => [qw/ vendor model os os_ver /],
order_by => ['vendor', 'model', { -desc => 'count' }, 'os_ver'],
})->hri->all;
template 'ajax/report/inventorybymodelbyos.tt', { results => \@results, },
{ layout => undef };
};
1;

View File

@@ -0,0 +1,168 @@
package App::Netdisco::Web::Plugin::Report::IpInventory;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use NetAddr::IP::Lite ':lower';
register_report(
{ category => 'IP',
tag => 'ipinventory',
label => 'IP Inventory',
provides_csv => 1,
}
);
get '/ajax/content/report/ipinventory' => require_login sub {
# Default to something simple with no results to prevent
# "Search failed!" error
my $subnet = param('subnet') || '0.0.0.0/32';
$subnet = NetAddr::IP::Lite->new($subnet);
$subnet = NetAddr::IP::Lite->new('0.0.0.0/32')
if (! $subnet) or ($subnet->addr eq '0.0.0.0');
my $agenot = param('age_invert') || '0';
my ( $start, $end ) = param('daterange') =~ /(\d+-\d+-\d+)/gmx;
my $limit = param('limit') || 256;
my $never = param('never') || '0';
my $order = [{-desc => 'age'}, {-asc => 'ip'}];
# We need a reasonable limit to prevent a potential DoS, especially if
# 'never' is true. TODO: Need better input validation, both JS and
# server-side to provide user feedback
$limit = 8192 if $limit > 8192;
my $rs1 = schema('netdisco')->resultset('DeviceIp')->search(
undef,
{ join => 'device',
select => [
'alias AS ip',
\'NULL as mac',
'creation AS time_first',
'device.last_discover AS time_last',
'dns',
\'true AS active',
\'false AS node',
\qq/replace( date_trunc( 'minute', age( now(), device.last_discover ) ) ::text, 'mon', 'month') AS age/
],
as => [qw( ip mac time_first time_last dns active node age)],
}
)->hri;
my $rs2 = schema('netdisco')->resultset('NodeIp')->search(
undef,
{ columns => [qw( ip mac time_first time_last dns active)],
'+select' => [ \'true AS node',
\qq/replace( date_trunc( 'minute', age( now(), time_last ) ) ::text, 'mon', 'month') AS age/
],
'+as' => [ 'node', 'age' ],
}
)->hri;
my $rs3 = schema('netdisco')->resultset('NodeNbt')->search(
undef,
{ columns => [qw( ip mac time_first time_last )],
'+select' => [
'nbname AS dns', 'active',
\'true AS node',
\qq/replace( date_trunc( 'minute', age( now(), time_last ) ) ::text, 'mon', 'month') AS age/
],
'+as' => [ 'dns', 'active', 'node', 'age' ],
}
)->hri;
my $rs_union = $rs1->union( [ $rs2, $rs3 ] );
if ( $never ) {
$subnet = NetAddr::IP::Lite->new('0.0.0.0/32') if ($subnet->bits ne 32);
my $rs4 = schema('netdisco')->resultset('Virtual::CidrIps')->search(
undef,
{ bind => [ $subnet->cidr ],
columns => [qw( ip mac time_first time_last dns active)],
'+select' => [ \'false AS node',
\qq/replace( date_trunc( 'minute', age( now(), time_last ) ) ::text, 'mon', 'month') AS age/
],
'+as' => [ 'node', 'age' ],
}
)->hri;
$rs_union = $rs_union->union( [$rs4] );
}
my $rs_sub = $rs_union->search(
{ ip => { '<<' => $subnet->cidr } },
{ select => [
\'DISTINCT ON (ip) ip',
'mac',
'dns',
\qq/date_trunc('second', time_last) AS time_last/,
\qq/date_trunc('second', time_first) AS time_first/,
'active',
'node',
'age'
],
as => [
'ip', 'mac', 'dns', 'time_last', 'time_first',
'active', 'node', 'age'
],
order_by => [{-asc => 'ip'}, {-desc => 'active'}],
}
)->as_query;
my $rs;
if ( $start && $end ) {
$start = $start . ' 00:00:00';
$end = $end . ' 23:59:59';
if ( $agenot ) {
$rs = $rs_union->search(
{ -or => [
time_first => [ undef ],
time_last => [ { '<', $start }, { '>', $end } ]
]
},
{ from => { me => $rs_sub }, }
);
}
else {
$rs = $rs_union->search(
{ -or => [
-and => [
time_first => undef,
time_last => undef,
],
-and => [
time_last => { '>=', $start },
time_last => { '<=', $end },
],
],
},
{ from => { me => $rs_sub }, }
);
}
}
else {
$rs = $rs_union->search( undef, { from => { me => $rs_sub }, } );
}
my @results = $rs->order_by($order)->limit($limit)->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/ipinventory.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/ipinventory_csv.tt', { results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,166 @@
package App::Netdisco::Web::Plugin::Report::ModuleInventory;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::ExpandParams 'expand_hash';
use App::Netdisco::Web::Plugin;
use List::MoreUtils ();
register_report(
{ category => 'Device',
tag => 'moduleinventory',
label => 'Module Inventory',
provides_csv => 1,
}
);
hook 'before' => sub {
return
unless (
request->path eq uri_for('/report/moduleinventory')->path
or index( request->path,
uri_for('/ajax/content/report/moduleinventory')->path ) == 0
);
# view settings
var('module_options' => [
{ name => 'fruonly',
label => 'FRU Only',
default => 'on'
},
{ name => 'matchall',
label => 'Match All Options',
default => 'on'
},
]
);
};
hook 'before_template' => sub {
my $tokens = shift;
return
unless (
request->path eq uri_for('/report/moduleinventory')->path
or index( request->path,
uri_for('/ajax/content/report/moduleinventory')->path ) == 0
);
# used in the search sidebar template to set selected items
foreach my $opt (qw/class/) {
my $p = (
ref [] eq ref param($opt)
? param($opt)
: ( param($opt) ? [ param($opt) ] : [] )
);
$tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p };
}
};
get '/ajax/content/report/moduleinventory/data' => require_login sub {
send_error( 'Missing parameter', 400 )
unless ( param('draw') && param('draw') =~ /\d+/ );
my $rs = schema('netdisco')->resultset('DeviceModule');
$rs = $rs->search( { -bool => 'fru' } ) if param('fruonly');
if ( param('device') ) {
my @ips = schema('netdisco')->resultset('Device')
->search_fuzzy( param('device') )->get_column('ip')->all;
params->{'ips'} = \@ips;
}
$rs = $rs->search_by_field( scalar params )->columns(
[ 'ip', 'description', 'name', 'class',
'type', 'serial', 'hw_ver', 'fw_ver',
'sw_ver', 'model'
]
)->search(
{},
{ '+columns' => [qw/ device.dns device.name /],
join => 'device',
collapse => 1,
}
);
my $exp_params = expand_hash( scalar params );
my $recordsTotal = $rs->count;
my @data = $rs->get_datatables_data($exp_params)->hri->all;
my $recordsFiltered = $rs->get_datatables_filtered_count($exp_params);
content_type 'application/json';
return to_json(
{ draw => int( param('draw') ),
recordsTotal => int($recordsTotal),
recordsFiltered => int($recordsFiltered),
data => \@data,
}
);
};
get '/ajax/content/report/moduleinventory' => require_login sub {
my $has_opt = List::MoreUtils::any { param($_) }
qw/device description name type model serial class/;
my $rs = schema('netdisco')->resultset('DeviceModule');
$rs = $rs->search( { -bool => 'fru' } ) if param('fruonly');
my @results;
if ( $has_opt && !request->is_ajax ) {
if ( param('device') ) {
my @ips = schema('netdisco')->resultset('Device')
->search_fuzzy( param('device') )->get_column('ip')->all;
params->{'ips'} = \@ips;
}
@results = $rs->search_by_field( scalar params )->columns(
[ 'ip', 'description', 'name', 'class',
'type', 'serial', 'hw_ver', 'fw_ver',
'sw_ver', 'model'
]
)->search(
{},
{ '+columns' => [qw/ device.dns device.name /],
join => 'device',
collapse => 1,
}
)->hri->all;
return unless scalar @results;
}
elsif ( !$has_opt ) {
@results = $rs->search(
{ class => { '!=', undef } },
{ select => [ 'class', { count => 'class' } ],
as => [qw/ class count /],
group_by => [qw/ class /]
}
)->order_by( { -desc => 'count' } )->hri->all;
return unless scalar @results;
}
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/moduleinventory.tt',
{ results => $json, opt => $has_opt },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/moduleinventory_csv.tt',
{ results => \@results, opt => $has_opt },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,110 @@
package App::Netdisco::Web::Plugin::Report::Netbios;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::ExpandParams 'expand_hash';
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Node',
tag => 'netbios',
label => 'NetBIOS Inventory',
provides_csv => 1,
}
);
hook 'before_template' => sub {
my $tokens = shift;
return
unless ( request->path eq uri_for('/report/netbios')->path
or
index( request->path, uri_for('/ajax/content/report/netbios')->path )
== 0 );
# used in the search sidebar template to set selected items
foreach my $opt (qw/domain/) {
my $p = (
ref [] eq ref param($opt)
? param($opt)
: ( param($opt) ? [ param($opt) ] : [] )
);
$tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p };
}
};
get '/ajax/content/report/netbios/data' => require_login sub {
send_error( 'Missing parameter', 400 )
unless ( param('draw') && param('draw') =~ /\d+/ );
my $domain = param('domain');
my $rs = schema('netdisco')->resultset('NodeNbt');
my $search = $domain eq 'blank' ? '' : $domain;
$rs = $rs->search( { domain => $search } )
->order_by( [ { -asc => 'domain' }, { -desc => 'time_last' } ] );
my $exp_params = expand_hash( scalar params );
my $recordsTotal = $rs->count;
my @data = $rs->get_datatables_data($exp_params)->hri->all;
my $recordsFiltered = $rs->get_datatables_filtered_count($exp_params);
content_type 'application/json';
return to_json(
{ draw => int( param('draw') ),
recordsTotal => int($recordsTotal),
recordsFiltered => int($recordsFiltered),
data => \@data,
}
);
};
get '/ajax/content/report/netbios' => require_login sub {
my $domain = param('domain');
my $rs = schema('netdisco')->resultset('NodeNbt');
my @results;
if ( defined $domain && !request->is_ajax ) {
my $search = $domain eq 'blank' ? '' : $domain;
@results
= $rs->search( { domain => $search } )
->order_by( [ { -asc => 'domain' }, { -desc => 'time_last' } ] )
->hri->all;
return unless scalar @results;
}
elsif ( !defined $domain ) {
@results = $rs->search(
{},
{ select => [ 'domain', { count => 'domain' } ],
as => [qw/ domain count /],
group_by => [qw/ domain /]
}
)->order_by( { -desc => 'count' } )->hri->all;
return unless scalar @results;
}
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/netbios.tt',
{ results => $json, opt => $domain },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/netbios_csv.tt',
{ results => \@results, opt => $domain },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,51 @@
package App::Netdisco::Web::Plugin::Report::NodeMultiIPs;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Node',
tag => 'nodemultiips',
label => 'Nodes with multiple active IP addresses',
provides_csv => 1,
}
);
get '/ajax/content/report/nodemultiips' => require_login sub {
my @results = schema('netdisco')->resultset('Node')->search(
{},
{ select => [ 'mac', 'switch', 'port' ],
join => [qw/device ips oui/],
'+columns' => [
{ 'dns' => 'device.dns' },
{ 'name' => 'device.name' },
{ 'ip_count' => { count => 'ips.ip' } },
{ 'vendor' => 'oui.company' }
],
group_by => [
qw/ me.mac me.switch me.port device.dns device.name oui.company/
],
having => \[ 'count(ips.ip) > ?', [ count => 1 ] ],
order_by => { -desc => [qw/count/] },
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/nodemultiips.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/nodemultiips_csv.tt',
{ results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,133 @@
package App::Netdisco::Web::Plugin::Report::NodeVendor;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::ExpandParams 'expand_hash';
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Node',
tag => 'nodevendor',
label => 'Node Vendor Inventory',
provides_csv => 1,
}
);
hook 'before_template' => sub {
my $tokens = shift;
return
unless (
request->path eq uri_for('/report/nodevendor')->path
or index( request->path,
uri_for('/ajax/content/report/nodevendor')->path ) == 0
);
# used in the search sidebar template to set selected items
foreach my $opt (qw/vendor/) {
my $p = (
ref [] eq ref param($opt)
? param($opt)
: ( param($opt) ? [ param($opt) ] : [] )
);
$tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p };
}
};
get '/ajax/content/report/nodevendor/data' => require_login sub {
send_error( 'Missing parameter', 400 )
unless ( param('draw') && param('draw') =~ /\d+/ );
my $vendor = param('vendor');
my $rs = schema('netdisco')->resultset('Node');
my $match = $vendor eq 'blank' ? undef : $vendor;
$rs = $rs->search( { 'oui.abbrev' => $match },
{ '+columns' => [qw/ device.dns device.name oui.abbrev /],
join => [qw/ oui device /],
collapse => 1,
});
unless ( param('archived') ) {
$rs = $rs->search( { -bool => 'me.active' } );
}
my $exp_params = expand_hash( scalar params );
my $recordsTotal = $rs->count;
my @data = $rs->get_datatables_data($exp_params)->hri->all;
my $recordsFiltered = $rs->get_datatables_filtered_count($exp_params);
content_type 'application/json';
return to_json(
{ draw => int( param('draw') ),
recordsTotal => int($recordsTotal),
recordsFiltered => int($recordsFiltered),
data => \@data,
}
);
};
get '/ajax/content/report/nodevendor' => require_login sub {
my $vendor = param('vendor');
my $rs = schema('netdisco')->resultset('Node');
my @results;
if ( defined $vendor && !request->is_ajax ) {
my $match = $vendor eq 'blank' ? undef : $vendor;
$rs = $rs->search( { 'oui.abbrev' => $match },
{ '+columns' => [qw/ device.dns device.name oui.abbrev /],
join => [qw/ oui device /],
collapse => 1,
});
unless ( param('archived') ) {
$rs = $rs->search( { -bool => 'me.active' } );
}
@results = $rs->hri->all;
return unless scalar @results;
}
elsif ( !defined $vendor ) {
$rs = $rs->search(
{ },
{ join => 'oui',
select => [ 'oui.abbrev', { count => 'me.mac' } ],
as => [qw/ vendor count /],
group_by => [qw/ oui.abbrev /]
}
)->order_by( { -desc => 'count' } );
unless ( param('archived') ) {
$rs = $rs->search( { -bool => 'me.active' } );
}
@results = $rs->hri->all;
return unless scalar @results;
}
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/nodevendor.tt',
{ results => $json, opt => $vendor },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/nodevendor_csv.tt',
{ results => \@results, opt => $vendor },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,54 @@
package App::Netdisco::Web::Plugin::Report::NodesDiscovered;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Web 'sql_match';
register_report(
{ category => 'Node',
tag => 'nodesdiscovered',
label => 'Nodes discovered through LLDP/CDP',
provides_csv => 1,
}
);
get '/ajax/content/report/nodesdiscovered' => require_login sub {
my $op = param('matchall') ? '-and' : '-or';
my @results = schema('netdisco')->resultset('Virtual::NodesDiscovered')
->search({
$op => [
(param('aps') ?
('me.remote_type' => { -ilike => 'AP:%' }) : ()),
(param('phones') ?
('me.remote_type' => { -ilike => '%ip_phone%' }) : ()),
(param('remote_id') ?
('me.remote_id' => { -ilike => scalar sql_match(param('remote_id')) }) : ()),
(param('remote_type') ? ('-or' => [
map {( 'me.remote_type' => { -ilike => scalar sql_match($_) } )}
grep { $_ }
(ref param('remote_type') ? @{param('remote_type')} : param('remote_type'))
]) : ()),
],
})
->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/nodesdiscovered.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/nodesdiscovered_csv.tt',
{ results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,45 @@
package App::Netdisco::Web::Plugin::Report::PortAdminDown;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Port',
tag => 'portadmindown',
label => 'Ports administratively disabled',
provides_csv => 1,
}
);
get '/ajax/content/report/portadmindown' => require_login sub {
my @results = schema('netdisco')->resultset('Device')->search(
{ 'up_admin' => 'down' },
{ select => [ 'ip', 'dns', 'name' ],
join => [ 'ports' ],
'+columns' => [
{ 'port' => 'ports.port' },
{ 'description' => 'ports.name' },
{ 'up_admin' => 'ports.up_admin' },
]
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json (\@results);
template 'ajax/report/portadmindown.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portadmindown_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,45 @@
package App::Netdisco::Web::Plugin::Report::PortBlocking;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Port',
tag => 'portblocking',
label => 'Ports that are blocking',
provides_csv => 1,
}
);
get '/ajax/content/report/portblocking' => require_login sub {
my @results = schema('netdisco')->resultset('Device')->search(
{ 'stp' => [ 'blocking', 'broken' ], 'up' => { '!=', 'down' } },
{ select => [ 'ip', 'dns', 'name' ],
join => ['ports'],
'+columns' => [
{ 'port' => 'ports.port' },
{ 'description' => 'ports.name' },
{ 'stp' => 'ports.stp' },
]
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json (\@results);
template 'ajax/report/portblocking.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portblocking_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,66 @@
package App::Netdisco::Web::Plugin::Report::PortLog;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report({
tag => 'portlog',
label => 'Port Control Log',
category => 'Port', # not used
hidden => true,
});
sub _sanity_ok {
return 0 unless
param('ip') =~ m/^[[:print:]]+$/
and param('port') =~ m/^[[:print:]]+$/
and param('log') =~ m/^[[:print:]]+$/;
return 1;
}
ajax '/ajax/control/report/portlog/add' => require_login sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $user = schema('netdisco')->resultset('DevicePortLog')
->create({
ip => param('ip'),
port => param('port'),
reason => 'other',
log => param('log'),
username => session('logged_in_user'),
userip => request->remote_address,
action => 'comment',
});
});
};
ajax '/ajax/content/report/portlog' => require_login sub {
my $device = param('q');
my $port = param('f');
send_error('Bad Request', 400) unless $device and $port;
$device = schema('netdisco')->resultset('Device')
->search_for_device($device);
return unless $device;
my $set = schema('netdisco')->resultset('DevicePortLog')->search({
ip => $device->ip,
port => $port,
}, {
order_by => { -desc => [qw/creation/] },
rows => 200,
})->with_times;
content_type('text/html');
template 'ajax/report/portlog.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,53 @@
package App::Netdisco::Web::Plugin::Report::PortMultiNodes;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Port',
tag => 'portmultinodes',
label => 'Ports with multiple nodes attached',
provides_csv => 1,
}
);
get '/ajax/content/report/portmultinodes' => require_login sub {
my @results = schema('netdisco')->resultset('Device')->search(
{ 'ports.remote_ip' => undef,
(param('vlan') ?
('ports.vlan' => param('vlan'), 'nodes.vlan' => param('vlan')) : ()),
'nodes.active' => 1,
'wireless.port' => undef
},
{ select => [ 'ip', 'dns', 'name' ],
join => { 'ports' => [ 'wireless', 'nodes' ] },
'+columns' => [
{ 'port' => 'ports.port' },
{ 'description' => 'ports.name' },
{ 'mac_count' => { count => 'nodes.mac' } },
],
group_by => [qw/me.ip me.dns me.name ports.port ports.name/],
having => \[ 'count(nodes.mac) > ?', [ count => 1 ] ],
order_by => { -desc => [qw/count/] },
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json (\@results);
template 'ajax/report/portmultinodes.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portmultinodes_csv.tt',
{ results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,79 @@
package App::Netdisco::Web::Plugin::Report::PortSsid;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Port',
tag => 'portssid',
label => 'Port SSID Inventory',
provides_csv => 1,
}
);
hook 'before_template' => sub {
my $tokens = shift;
return
unless (
request->path eq uri_for('/report/portssid')->path
or index(
request->path, uri_for('/ajax/content/report/portssid')->path
) == 0
);
# used in the search sidebar template to set selected items
foreach my $opt (qw/ssid/) {
my $p = (
ref [] eq ref param($opt)
? param($opt)
: ( param($opt) ? [ param($opt) ] : [] )
);
$tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p };
}
};
get '/ajax/content/report/portssid' => require_login sub {
my $ssid = param('ssid');
my $rs = schema('netdisco')->resultset('DevicePortSsid');
if ( defined $ssid ) {
$rs = $rs->search(
{ ssid => $ssid },
{ '+columns' => [
qw/ device.dns device.name device.model device.vendor port.port/
],
join => [qw/ device port /],
collapse => 1,
}
)->order_by( [qw/ port.ip port.port /] )->hri;
}
else {
$rs = $rs->get_ssids->hri;
}
my @results = $rs->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/portssid.tt',
{ results => $json, opt => $ssid },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portssid_csv.tt',
{ results => \@results, opt => $ssid },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,33 @@
package App::Netdisco::Web::Plugin::Report::PortUtilization;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Device',
tag => 'portutilization',
label => 'Port Utilization',
provides_csv => 1,
}
);
get '/ajax/content/report/portutilization' => require_login sub {
return unless schema('netdisco')->resultset('Device')->count;
my @results = schema('netdisco')->resultset('Virtual::PortUtilization')->hri->all;
if (request->is_ajax) {
my $json = to_json (\@results);
template 'ajax/report/portutilization.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portutilization_csv.tt', { results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,35 @@
package App::Netdisco::Web::Plugin::Report::SsidInventory;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'Wireless',
tag => 'ssidinventory',
label => 'SSID Inventory',
provides_csv => 1,
}
);
get '/ajax/content/report/ssidinventory' => require_login sub {
my @results = schema('netdisco')->resultset('DevicePortSsid')
->get_ssids->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/report/portssid.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/portssid_csv.tt', { results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,42 @@
package App::Netdisco::Web::Plugin::Report::SubnetUtilization;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report({
category => 'IP',
tag => 'subnets',
label => 'Subnet Utilization',
provides_csv => 1,
});
get '/ajax/content/report/subnets' => require_login sub {
my $subnet = param('subnet') || '0.0.0.0/32';
my $agenot = param('age_invert') || '0';
my ( $start, $end ) = param('daterange') =~ /(\d+-\d+-\d+)/gmx;
$start = $start . ' 00:00:00';
$end = $end . ' 23:59:59';
my @results = schema('netdisco')->resultset('Virtual::SubnetUtilization')
->search(undef,{
bind => [ $subnet, $start, $end, $start, $subnet, $start, $start ],
})->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
template 'ajax/report/subnets.tt', { results => \@results },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/subnets_csv.tt', { results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,46 @@
package App::Netdisco::Web::Plugin::Report::VlanInventory;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_report(
{ category => 'VLAN',
tag => 'vlaninventory',
label => 'VLAN Inventory',
provides_csv => 1,
}
);
get '/ajax/content/report/vlaninventory' => require_login sub {
my @results = schema('netdisco')->resultset('DeviceVlan')->search(
{ 'me.description' => { '!=', 'NULL' } },
{ join => { 'ports' => 'vlan' },
select => [
'me.vlan',
'me.description',
{ count => { distinct => 'me.ip' } },
{ count => 'ports.vlan' }
],
as => [qw/ vlan description dcount pcount /],
group_by => [qw/ me.vlan me.description /],
}
)->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json (\@results);
template 'ajax/report/vlaninventory.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/report/vlaninventory_csv.tt', { results => \@results },
{ layout => undef };
}
};
true;

View File

@@ -0,0 +1,53 @@
package App::Netdisco::Web::Plugin::Search::Device;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use List::MoreUtils ();
use App::Netdisco::Web::Plugin;
register_search_tab(
{ tag => 'device', label => 'Device', provides_csv => 1 } );
# device with various properties or a default match-all
get '/ajax/content/search/device' => require_login sub {
my $has_opt = List::MoreUtils::any { param($_) }
qw/name location dns ip description model os os_ver vendor layers/;
my $rs;
if ($has_opt) {
$rs = schema('netdisco')->resultset('Device')->columns(
[ "ip", "dns", "name", "contact",
"location", "model", "os_ver", "serial"
]
)->with_times->search_by_field( scalar params );
}
else {
my $q = param('q');
send_error( 'Missing query', 400 ) unless $q;
$rs = schema('netdisco')->resultset('Device')->columns(
[ "ip", "dns", "name", "contact",
"location", "model", "os_ver", "serial"
]
)->with_times->search_fuzzy($q);
}
my @results = $rs->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/search/device.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/search/device_csv.tt', { results => \@results, },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,184 @@
package App::Netdisco::Web::Plugin::Search::Node;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use NetAddr::IP::Lite ':lower';
use NetAddr::MAC ();
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Web 'sql_match';
register_search_tab({ tag => 'node', label => 'Node' });
# nodes matching the param as an IP or DNS hostname or MAC
ajax '/ajax/content/search/node' => require_login sub {
my $node = param('q');
send_error('Missing node', 400) unless $node;
content_type('text/html');
my $agenot = param('age_invert') || '0';
my ( $start, $end ) = param('daterange') =~ m/(\d+-\d+-\d+)/gmx;
my $mac = NetAddr::MAC->new(mac => $node);
my @active = (param('archived') ? () : (-bool => 'active'));
my (@times, @wifitimes, @porttimes);
if ($start and $end) {
$start = $start . ' 00:00:00';
$end = $end . ' 23:59:59';
if ($agenot) {
@times = (-or => [
time_first => [ { '<', $start }, undef ],
time_last => { '>', $end },
]);
@wifitimes = (-or => [
time_last => { '<', $start },
time_last => { '>', $end },
]);
@porttimes = (-or => [
creation => { '<', $start },
creation => { '>', $end },
]);
}
else {
@times = (-and => [
time_first => { '>=', $start },
time_last => { '<=', $end },
]);
@wifitimes = (-and => [
time_last => { '>=', $start },
time_last => { '<=', $end },
]);
@porttimes = (-and => [
creation => { '>=', $start },
creation => { '<=', $end },
]);
}
}
my ($likeval, $likeclause) = sql_match($node, not param('partial'));
my $using_wildcards = (($likeval ne $node) ? 1 : 0);
my @where_mac =
($using_wildcards ? \['me.mac::text ILIKE ?', $likeval]
: ((!defined $mac or $mac->errstr) ? \'0=1' : ('me.mac' => $mac->as_ieee)) );
my $sightings = schema('netdisco')->resultset('Node')
->search({-and => [@where_mac, @active, @times]}, {
order_by => {'-desc' => 'time_last'},
'+columns' => [
'device.dns',
{ time_first_stamp => \"to_char(time_first, 'YYYY-MM-DD HH24:MI')" },
{ time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')" },
],
join => 'device',
});
my $ips = schema('netdisco')->resultset('NodeIp')
->search({-and => [@where_mac, @active, @times]}, {
order_by => {'-desc' => 'time_last'},
'+columns' => [
'oui.company',
'oui.abbrev',
{ time_first_stamp => \"to_char(time_first, 'YYYY-MM-DD HH24:MI')" },
{ time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')" },
],
join => 'oui'
});
my $netbios = schema('netdisco')->resultset('NodeNbt')
->search({-and => [@where_mac, @active, @times]}, {
order_by => {'-desc' => 'time_last'},
'+columns' => [
'oui.company',
'oui.abbrev',
{ time_first_stamp => \"to_char(time_first, 'YYYY-MM-DD HH24:MI')" },
{ time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')" },
],
join => 'oui'
});
my $wireless = schema('netdisco')->resultset('NodeWireless')->search(
{ -and => [@where_mac, @wifitimes] },
{ order_by => { '-desc' => 'time_last' },
'+columns' => [
'oui.company',
'oui.abbrev',
{
time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')"
}],
join => 'oui'
}
);
my $rs_dp = schema('netdisco')->resultset('DevicePort');
if ($sightings->has_rows or $ips->has_rows or $netbios->has_rows) {
my $ports = param('deviceports')
? $rs_dp->search({ -and => [@where_mac] }) : undef;
return template 'ajax/search/node_by_mac.tt', {
ips => $ips,
sightings => $sightings,
ports => $ports,
wireless => $wireless,
netbios => $netbios,
}, { layout => undef };
}
else {
my $ports = param('deviceports')
? $rs_dp->search({ -and => [@where_mac, @porttimes] }) : undef;
if (defined $ports and $ports->has_rows) {
return template 'ajax/search/node_by_mac.tt', {
ips => $ips,
sightings => $sightings,
ports => $ports,
wireless => $wireless,
netbios => $netbios,
}, { layout => undef };
}
}
my $set = schema('netdisco')->resultset('NodeNbt')
->search_by_name({nbname => $likeval, @active, @times});
unless ( $set->has_rows ) {
if (my $ip = NetAddr::IP::Lite->new($node)) {
# search_by_ip() will extract cidr notation if necessary
$set = schema('netdisco')->resultset('NodeIp')
->search_by_ip({ip => $ip, @active, @times});
}
else {
$likeval .= setting('domain_suffix')
if index($node, setting('domain_suffix')) == -1;
$set = schema('netdisco')->resultset('NodeIp')
->search_by_dns({dns => $likeval, @active, @times});
# if the user selects Vendor search opt, then
# we'll try the OUI company name as a fallback
if (param('show_vendor') and not $set->has_rows) {
$set = schema('netdisco')->resultset('NodeIp')
->with_times
->search(
{'oui.company' => { -ilike => ''.sql_match($node)}, @times},
{'prefetch' => 'oui'},
);
}
}
}
return unless $set and $set->has_rows;
$set = $set->search_rs({}, { order_by => 'me.mac' });
template 'ajax/search/node_by_ip.tt', {
macs => $set,
archive_filter => {@active},
}, { layout => undef };
};
true;

View File

@@ -0,0 +1,65 @@
package App::Netdisco::Web::Plugin::Search::Port;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Web 'sql_match';
register_search_tab( { tag => 'port', label => 'Port', provides_csv => 1 } );
# device ports with a description (er, name) matching
get '/ajax/content/search/port' => require_login sub {
my $q = param('q');
send_error( 'Missing query', 400 ) unless $q;
my $rs;
if ( $q =~ m/^\d+$/ ) {
$rs
= schema('netdisco')->resultset('DevicePort')
->columns( [qw/ ip port name descr /] )->search(
{ "port_vlans.vlan" => $q },
{ '+columns' => [qw/ device.dns device.name port_vlans.vlan /],
join => [qw/ port_vlans device /]
}
);
}
else {
my ( $likeval, $likeclause ) = sql_match($q);
$rs
= schema('netdisco')->resultset('DevicePort')
->columns( [qw/ ip port name descr /] )->search(
{ -or => [
{ "me.name" => ( param('partial') ? $likeclause : $q ) },
( length $q == 17
? { "me.mac" => $q }
: \[ 'me.mac::text ILIKE ?', $likeval ]
),
{ "me.remote_id" => $likeclause },
{ "me.remote_type" => $likeclause },
]
},
{ '+columns' => [qw/ device.dns device.name port_vlans.vlan /],
join => [qw/ port_vlans device /]
}
);
}
my @results = $rs->hri->all;
return unless scalar @results;
if ( request->is_ajax ) {
my $json = to_json( \@results );
template 'ajax/search/port.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/search/port_csv.tt', { results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,41 @@
package App::Netdisco::Web::Plugin::Search::VLAN;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
register_search_tab( { tag => 'vlan', label => 'VLAN', provides_csv => 1 } );
# devices carrying vlan xxx
get '/ajax/content/search/vlan' => require_login sub {
my $q = param('q');
send_error( 'Missing query', 400 ) unless $q;
my $rs;
if ( $q =~ m/^\d+$/ ) {
$rs = schema('netdisco')->resultset('Device')
->carrying_vlan( { vlan => $q } );
}
else {
$rs = schema('netdisco')->resultset('Device')
->carrying_vlan_name( { name => $q } );
}
my @results = $rs->hri->all;
return unless scalar @results;
if (request->is_ajax) {
my $json = to_json( \@results );
template 'ajax/search/vlan.tt', { results => $json },
{ layout => undef };
}
else {
header( 'Content-Type' => 'text/comma-separated-values' );
template 'ajax/search/vlan_csv.tt', { results => \@results },
{ layout => undef };
}
};
1;

View File

@@ -0,0 +1,88 @@
package App::Netdisco::Web::PortControl;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::JobQueue qw/jq_insert jq_userlog/;
ajax '/ajax/portcontrol' => require_role port_control => sub {
send_error('No device/port/field', 400)
unless param('device') and (param('port') or param('field'));
my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]',
param('device'), (param('port') || ''), param('field'),
(param('action') || ''), (param('value') || '');
my %action_map = (
'location' => 'location',
'contact' => 'contact',
'c_port' => 'portcontrol',
'c_name' => 'portname',
'c_pvid' => 'vlan',
'c_power' => 'power',
);
my $action = $action_map{ param('field') };
my $subaction = ($action =~ m/^(?:power|portcontrol)/
? (param('action') ."-other")
: param('value'));
schema('netdisco')->txn_do(sub {
if (param('port')) {
my $a = "$action $subaction";
$a =~ s/-other$//;
$a =~ s/^portcontrol/port/;
schema('netdisco')->resultset('DevicePortLog')->create({
ip => param('device'),
port => param('port'),
action => $a,
username => session('logged_in_user'),
userip => request->remote_address,
reason => (param('reason') || 'other'),
log => param('log'),
});
}
jq_insert({
device => param('device'),
port => param('port'),
action => $action,
subaction => $subaction,
username => session('logged_in_user'),
userip => request->remote_address,
log => $log,
});
});
content_type('application/json');
to_json({});
};
ajax '/ajax/userlog' => require_login sub {
my @jobs = jq_userlog( session('logged_in_user') );
my %status = (
'done' => [
map {s/\[\]/&lt;empty&gt;/; $_}
map { $_->log }
grep { $_->status eq 'done' }
grep { defined }
@jobs
],
'error' => [
map {s/\[\]/&lt;empty&gt;/; $_}
map { $_->log }
grep { $_->status eq 'error' }
grep { defined }
@jobs
],
);
content_type('application/json');
to_json(\%status);
};
true;

View File

@@ -0,0 +1,66 @@
package App::Netdisco::Web::Report;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
get '/report/*' => require_login sub {
my ($tag) = splat;
# used in the report search sidebar to populate select inputs
my ( $domain_list, $class_list, $ssid_list, $type_list, $vendor_list );
if ( $tag eq 'netbios' ) {
$domain_list = [ schema('netdisco')->resultset('NodeNbt')
->get_distinct_col('domain') ];
}
elsif ( $tag eq 'moduleinventory' ) {
$class_list = [ schema('netdisco')->resultset('DeviceModule')
->get_distinct_col('class') ];
# this is a bit fragile... three params currently
my %params = request->params();
if (3 == scalar keys %params) {
foreach my $col ( @{ var('module_options') } ) {
next unless $col->{default} eq 'on';
$params{ $col->{name} } = 'checked';
}
}
}
elsif ( $tag eq 'portssid' ) {
$ssid_list = [ schema('netdisco')->resultset('DevicePortSsid')
->get_distinct_col('ssid') ];
}
elsif ( $tag eq 'nodesdiscovered' ) {
$type_list = [ schema('netdisco')->resultset('DevicePort')
->get_distinct_col('remote_type') ];
}
elsif ( $tag eq 'nodevendor' ) {
$vendor_list = [
schema('netdisco')->resultset('Node')->search(
{},
{ join => 'oui',
columns => ['oui.abbrev'],
order_by => 'oui.abbrev',
group_by => 'oui.abbrev',
}
)->get_column('abbrev')->all
];
}
# trick the ajax into working as if this were a tabbed page
params->{tab} = $tag;
var( nav => 'reports' );
template 'report',
{
report => setting('_reports')->{$tag},
domain_list => $domain_list,
class_list => $class_list,
ssid_list => $ssid_list,
type_list => $type_list,
vendor_list => $vendor_list,
};
};
true;

View File

@@ -0,0 +1,131 @@
package App::Netdisco::Web::Search;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::Web 'sql_match';
use NetAddr::MAC ();
hook 'before' => sub {
# view settings for node options
var('node_options' => [
{ name => 'stamps', label => 'Time Stamps', default => 'on' },
{ name => 'deviceports', label => 'Device Ports', default => 'on' },
]);
# view settings for device options
var('device_options' => [
{ name => 'matchall', label => 'Match All Options', default => 'on' },
]);
return unless (request->path eq uri_for('/search')->path
or index(request->path, uri_for('/ajax/content/search')->path) == 0);
foreach my $col (@{ var('node_options') }) {
next unless $col->{default} eq 'on';
params->{$col->{name}} = 'checked'
if not param('tab') or param('tab') ne 'node';
}
foreach my $col (@{ var('device_options') }) {
next unless $col->{default} eq 'on';
params->{$col->{name}} = 'checked'
if not param('tab') or param('tab') ne 'device';
}
};
hook 'before_template' => sub {
my $tokens = shift;
# new searches will use these defaults in their sidebars
$tokens->{search_node} = uri_for('/search', {tab => 'node'});
$tokens->{search_device} = uri_for('/search', {tab => 'device'});
foreach my $col (@{ var('node_options') }) {
next unless $col->{default} eq 'on';
$tokens->{search_node}->query_param($col->{name}, 'checked');
}
foreach my $col (@{ var('device_options') }) {
next unless $col->{default} eq 'on';
$tokens->{search_device}->query_param($col->{name}, 'checked');
}
return unless (request->path eq uri_for('/search')->path
or index(request->path, uri_for('/ajax/content/search')->path) == 0);
# used in the device search sidebar template to set selected items
foreach my $opt (qw/model vendor os os_ver/) {
my $p = (ref [] eq ref param($opt) ? param($opt)
: (param($opt) ? [param($opt)] : []));
$tokens->{"${opt}_lkp"} = { map { $_ => 1 } @$p };
}
};
get '/search' => require_login sub {
my $q = param('q');
my $s = schema('netdisco');
if (not param('tab')) {
if (not $q) {
return redirect uri_for('/')->path;
}
# pick most likely tab for initial results
if ($q =~ m/^\d+$/) {
params->{'tab'} = 'vlan';
}
else {
my $nd = $s->resultset('Device')->search_fuzzy($q);
my ($likeval, $likeclause) = sql_match($q);
my $mac = NetAddr::MAC->new($q);
if ($nd and $nd->count) {
if ($nd->count == 1) {
# redirect to device details for the one device
return redirect uri_for('/device', {
tab => 'details',
q => $nd->first->ip,
f => '',
})->path_query;
}
# multiple devices
params->{'tab'} = 'device';
}
elsif ($s->resultset('DevicePort')
->search({
-or => [
{name => $likeclause},
((!defined $mac or $mac->errstr)
? \['mac::text ILIKE ?', $likeval]
: {mac => $mac->as_ieee}),
],
})->count) {
params->{'tab'} = 'port';
}
}
# if all else fails
params->{'tab'} ||= 'node';
}
# used in the device search sidebar to populate select inputs
my $model_list = [ $s->resultset('Device')->get_distinct_col('model') ];
my $os_list = [ $s->resultset('Device')->get_distinct_col('os') ];
my $os_ver_list = [ $s->resultset('Device')->get_distinct_col('os_ver') ];
my $vendor_list = [ $s->resultset('Device')->get_distinct_col('vendor') ];
template 'search', {
search => params->{'tab'},
model_list => $model_list,
os_list => $os_list,
os_ver_list => $os_ver_list,
vendor_list => $vendor_list,
};
};
true;

View File

@@ -0,0 +1,30 @@
package App::Netdisco::Web::Static;
use Dancer ':syntax';
use Path::Class;
get '/plugin/*/*.js' => sub {
my ($plugin) = splat;
my $content = template
'plugin.tt', { target => "plugin/$plugin/$plugin.js" },
{ layout => undef };
send_file \$content,
content_type => 'application/javascript',
filename => "$plugin.js";
};
get '/plugin/*/*.css' => sub {
my ($plugin) = splat;
my $content = template
'plugin.tt', { target => "plugin/$plugin/$plugin.css" },
{ layout => undef };
send_file \$content,
content_type => 'text/css',
filename => "$plugin.css";
};
true;

View File

@@ -0,0 +1,85 @@
package App::Netdisco::Web::Statistics;
use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
get '/ajax/content/statistics' => require_login sub {
my $time1 = time;
my $schema = schema('netdisco');
my $devices = $schema->resultset('Device');
# used only to get the PostgreSQL version
my $users = $schema->resultset('User')->search(
{},
{ select => [ { version => '' } ],
as => [qw/ version /],
}
);
my $device_count = $devices->count;
my $device_port_count = $schema->resultset('DevicePort')->count;
my $device_ip_count = $schema->resultset('DeviceIp')
->search( undef, { columns => [qw/ alias /] } )->count;
my $nodes = $schema->resultset('Node')->search(
{},
{ columns => [qw/mac/],
distinct => 1
}
);
my $node_count = $nodes->count;
my $node_table_count = $schema->resultset('Node')->count;
my $nodes_ips = $schema->resultset('NodeIp')->search(
{},
{ columns => [qw/ip/],
distinct => 1
}
);
my $ip_count = $nodes_ips->count;
my $ip_table_count
= $schema->resultset('NodeIp')->search( {}, { columns => [qw/ip/] } )
->count;
my $device_links = $schema->resultset('DevicePort')
->search( { 'remote_ip' => { '!=', undef } } )->count;
my $schema_version = $schema->get_db_version;
my $target_version = $schema->schema_version;
my $time2 = time;
my $process_time = $time2 - $time1;
my $disco_ver = $App::Netdisco::VERSION;
my $db_version = $users->next->get_column('version');
my $dbi_ver = $DBI::VERSION;
my $dbdpg_ver = $DBD::Pg::VERSION;
eval { require SNMP::Info };
my $snmpinfo_ver = ($@ ? 'n/a' : $SNMP::Info::VERSION);
var( nav => 'statistics' );
template 'ajax/statistics.tt',
{
device_count => $device_count,
device_ip_count => $device_ip_count,
device_links => $device_links,
device_port_count => $device_port_count,
ip_count => $ip_count,
ip_table_count => $ip_table_count,
node_count => $node_count,
node_table_count => $node_table_count,
process_time => $process_time,
disco_ver => $disco_ver,
db_version => $db_version,
dbi_ver => $dbi_ver,
dbdpg_ver => $dbdpg_ver,
snmpinfo_ver => $snmpinfo_ver,
schema_ver => $schema_version,
},
{ layout => undef };
};
true;

View File

@@ -0,0 +1,71 @@
package App::Netdisco::Web::TypeAhead;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Util::Web (); # for sort_port
ajax '/ajax/data/devicename/typeahead' => require_login sub {
return '[]' unless setting('navbar_autocomplete');
my $q = param('query') || param('term');
my $set = schema('netdisco')->resultset('Device')->search_fuzzy($q);
content_type 'application/json';
to_json [map {$_->dns || $_->name || $_->ip} $set->all];
};
ajax '/ajax/data/deviceip/typeahead' => require_login sub {
my $q = param('query') || param('term');
my $set = schema('netdisco')->resultset('Device')->search_fuzzy($q);
my @data = ();
while (my $d = $set->next) {
my $label = $d->ip;
if ($d->dns or $d->name) {
$label = sprintf '%s (%s)',
($d->dns || $d->name), $d->ip;
}
push @data, { label => $label, value => $d->ip };
}
content_type 'application/json';
to_json \@data;
};
ajax '/ajax/data/port/typeahead' => require_login sub {
my $dev = param('dev1') || param('dev2');
my $port = param('port1') || param('port2');
send_error('Missing device', 400) unless $dev;
my $device = schema('netdisco')->resultset('Device')
->find({ip => $dev});
send_error('Bad device', 400) unless $device;
my $set = $device->ports({},{order_by => 'port'});
$set = $set->search({port => { -ilike => "\%$port\%" }})
if $port;
my $results = [
map {{ label => (sprintf "%s (%s)", $_->port, $_->name), value => $_->port }}
sort { &App::Netdisco::Util::Web::sort_port($a->port, $b->port) } $set->all
];
content_type 'application/json';
to_json \@$results;
};
ajax '/ajax/data/subnet/typeahead' => require_login sub {
my $q = param('query') || param('term');
$q = "$q\%" if $q !~ m/\%/;
my $nets = schema('netdisco')->resultset('Subnet')->search(
{ 'me.net::text' => { '-ilike' => $q }},
{ columns => ['net'], order_by => 'net' } );
content_type 'application/json';
to_json [map {$_->net} $nets->all];
};
true;