From e5820070fcb67bf80c3a629853d433e0b809b712 Mon Sep 17 00:00:00 2001 From: "Eric A. Miller" Date: Mon, 20 Jan 2014 23:38:14 -0500 Subject: [PATCH] #68 Devices orphaned by missing topology info report --- Netdisco/Changes | 1 + .../DB/Result/Virtual/OrphanedDevices.pm | 32 ++++++ .../DB/Result/Virtual/UnDirEdgesAgg.pm | 54 +++++++++ .../Web/Plugin/AdminTask/OrphanedDevices.pm | 82 ++++++++++++++ Netdisco/share/config.yml | 1 + .../share/views/ajax/admintask/orphaned.tt | 105 ++++++++++++++++++ .../views/ajax/admintask/orphaned_csv.tt | 51 +++++++++ 7 files changed, 326 insertions(+) create mode 100644 Netdisco/lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm create mode 100644 Netdisco/lib/App/Netdisco/DB/Result/Virtual/UnDirEdgesAgg.pm create mode 100644 Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/OrphanedDevices.pm create mode 100644 Netdisco/share/views/ajax/admintask/orphaned.tt create mode 100644 Netdisco/share/views/ajax/admintask/orphaned_csv.tt diff --git a/Netdisco/Changes b/Netdisco/Changes index 09a064cc..d52a9fc3 100644 --- a/Netdisco/Changes +++ b/Netdisco/Changes @@ -3,6 +3,7 @@ [NEW FEATURES] * Support for Link Aggregation (port-channel, etherchannel, "trunking", etc) + * [#68] Devices orphaned by missing topology info report [ENHANCEMENTS] diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm new file mode 100644 index 00000000..d77bf90f --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm @@ -0,0 +1,32 @@ +package App::Netdisco::DB::Result::Virtual::OrphanedDevices; + +use strict; +use warnings; + +use utf8; +use base 'App::Netdisco::DB::Result::Device'; + +__PACKAGE__->load_components('Helper::Row::SubClass'); +__PACKAGE__->subclass; + +__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); +__PACKAGE__->table('orphaned_devices'); +__PACKAGE__->result_source_instance->is_virtual(1); +__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL'); +SELECT * +FROM device +WHERE ip NOT IN + ( SELECT DISTINCT dp.ip AS ip + FROM + (SELECT device_port.ip, + device_port.remote_ip + FROM device_port + WHERE device_port.remote_port IS NOT NULL + GROUP BY device_port.ip, + device_port.remote_ip + ORDER BY device_port.ip) dp + LEFT JOIN device_ip di ON dp.remote_ip = di.alias + WHERE di.ip IS NOT NULL) +ENDSQL + +1; diff --git a/Netdisco/lib/App/Netdisco/DB/Result/Virtual/UnDirEdgesAgg.pm b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/UnDirEdgesAgg.pm new file mode 100644 index 00000000..8a304758 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/Result/Virtual/UnDirEdgesAgg.pm @@ -0,0 +1,54 @@ +package App::Netdisco::DB::Result::Virtual::UnDirEdgesAgg; + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +__PACKAGE__->table_class('DBIx::Class::ResultSource::View'); + +__PACKAGE__->table('undir_edges_agg'); +__PACKAGE__->result_source_instance->is_virtual(1); +__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL'); + SELECT left_ip, + array_agg(right_ip) AS links + FROM + ( SELECT dp.ip AS left_ip, + di.ip AS right_ip + FROM + (SELECT device_port.ip, + device_port.remote_ip + FROM device_port + WHERE device_port.remote_port IS NOT NULL + GROUP BY device_port.ip, + device_port.remote_ip) dp + LEFT JOIN device_ip di ON dp.remote_ip = di.alias + WHERE di.ip IS NOT NULL + UNION SELECT di.ip AS left_ip, + dp.ip AS right_ip + FROM + (SELECT device_port.ip, + device_port.remote_ip + FROM device_port + WHERE device_port.remote_port IS NOT NULL + GROUP BY device_port.ip, + device_port.remote_ip) dp + LEFT JOIN device_ip di ON dp.remote_ip = di.alias + WHERE di.ip IS NOT NULL ) AS foo + GROUP BY left_ip + ORDER BY left_ip +ENDSQL + +__PACKAGE__->add_columns( + 'left_ip' => { + data_type => 'inet', + }, + 'links' => { + data_type => 'inet[]', + } +); + +__PACKAGE__->belongs_to('device', 'App::Netdisco::DB::Result::Device', + { 'foreign.ip' => 'self.left_ip' }); + +1; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/OrphanedDevices.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/OrphanedDevices.pm new file mode 100644 index 00000000..949e7a38 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/OrphanedDevices.pm @@ -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; diff --git a/Netdisco/share/config.yml b/Netdisco/share/config.yml index 3f1f8dcf..877dd1d0 100644 --- a/Netdisco/share/config.yml +++ b/Netdisco/share/config.yml @@ -58,6 +58,7 @@ web_plugins: - AdminTask::PseudoDevice - AdminTask::SlowDevices - AdminTask::UndiscoveredNeighbors + - AdminTask::OrphanedDevices - AdminTask::UserLog - AdminTask::Users - Search::Device diff --git a/Netdisco/share/views/ajax/admintask/orphaned.tt b/Netdisco/share/views/ajax/admintask/orphaned.tt new file mode 100644 index 00000000..b87ebeec --- /dev/null +++ b/Netdisco/share/views/ajax/admintask/orphaned.tt @@ -0,0 +1,105 @@ +[% IF orphans.size > 0 %] +
+
+ +
+
+ + + + + + + + + + + + [% FOREACH row IN orphans %] + + + + + + + + [%END%] + +
DeviceLocationContactVendorModel
+ [% row.dns || row.name || row.ip | html_entity %] + [% IF row.location %] + + [% row.location | html_entity %] + [% ELSE %] + [Not Set] + [% END %] + [% row.contact | html_entity %][% row.vendor | html_entity %][% row.model | html_entity %]
+
+
+
+
+[% END %] +[%# The largest graph is considered the main network, all others are + considered orphaned, so we need two to generate div %] +[% IF graphs.size > 1 %] +
+[% count = 0 %] +[% FOREACH network IN graphs %] + [% count = count + 1 %] + [%# The largest is not an orphan, so skip %] + [% NEXT IF count == 1 %] +
+ +
+
+ + + + + + + + + + + + [% FOREACH row IN network %] + + + + + + + + [% END %] + +
DeviceLocationContactVendorModel
+ [% row.dns || row.name || row.ip | html_entity %] + [% IF row.location %] + + [% row.location | html_entity %] + [% ELSE %] + [Not Set] + [% END %] + [% row.contact | html_entity %][% row.vendor | html_entity %][% row.model | html_entity %]
+
+
+
+[% END %] +
+[% END %] + diff --git a/Netdisco/share/views/ajax/admintask/orphaned_csv.tt b/Netdisco/share/views/ajax/admintask/orphaned_csv.tt new file mode 100644 index 00000000..a87372f0 --- /dev/null +++ b/Netdisco/share/views/ajax/admintask/orphaned_csv.tt @@ -0,0 +1,51 @@ +[% USE CSV -%] +[% CSV.dump(['Orphaned Devices']) %] + +[% CSV.dump([ 'Device' 'IP' 'Device Location' 'Contact' ' Vendor' + 'Model' ]) %] + +[% FOREACH row IN orphans %] + [% mydlist = [] %] + [% mydevice = row.dns || row.name %] + [% mydlist.push(mydevice) %] + [% mydlist.push(row.ip) %] + [% mydlist.push(row.location) %] + [% mydlist.push(row.contact) %] + [% mydlist.push(row.vendor) %] + [% mydlist.push(row.model) %] + [% CSV.dump(mydlist) %] + +[% END %] + +[% IF graphs.size > 1 %] +[% count = 0 %] +[% FOREACH network IN graphs %] + [% count = count + 1 %] + [%# The largest is not an orphan, so skip %] + [% NEXT IF count == 1 %] + + [% CSV.dump([' ']) %] + + [% ntwk_header = [] %] + [% ntwk_header.push('Orphaned Network') %] + [% ntwk_header.push(count - 1) %] + + [% CSV.dump(ntwk_header) %] + + [% CSV.dump([ 'Device' 'IP' 'Device Location' 'Contact' ' Vendor' + 'Model' ]) %] + + [% FOREACH row IN network %] + [% mydlist = [] %] + [% mydevice = row.dns || row.name %] + [% mydlist.push(mydevice) %] + [% mydlist.push(row.ip) %] + [% mydlist.push(row.location) %] + [% mydlist.push(row.contact) %] + [% mydlist.push(row.vendor) %] + [% mydlist.push(row.model) %] + [% CSV.dump(mydlist) %] + + [% END %] +[% END %] +[% END %] \ No newline at end of file