#68 Devices orphaned by missing topology info report

This commit is contained in:
Eric A. Miller
2014-01-20 23:38:14 -05:00
parent 9ed92c85f7
commit e5820070fc
7 changed files with 326 additions and 0 deletions

View File

@@ -3,6 +3,7 @@
[NEW FEATURES]
* Support for Link Aggregation (port-channel, etherchannel, "trunking", etc)
* [#68] Devices orphaned by missing topology info report
[ENHANCEMENTS]

View File

@@ -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;

View File

@@ -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;

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

@@ -58,6 +58,7 @@ web_plugins:
- AdminTask::PseudoDevice
- AdminTask::SlowDevices
- AdminTask::UndiscoveredNeighbors
- AdminTask::OrphanedDevices
- AdminTask::UserLog
- AdminTask::Users
- Search::Device

View File

@@ -0,0 +1,105 @@
[% IF orphans.size > 0 %]
<div class="accordion" id="accordion-orphans">
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-target="#collapse-orphan" href="#collapse-orphan">
<i class="icon-chevron-up"></i> &nbsp;
Orphaned Devices
</a>
</div>
<div id="collapse-orphan" class="accordion-body collapse">
<div class="accordion-inner">
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th>Device</th>
<th>Location</th>
<th>Contact</th>
<th>Vendor</th>
<th>Model</th>
</tr>
</thead>
<tbody>
[% FOREACH row IN orphans %]
<tr>
<td><a href="[% uri_for('/device') %]?q=[% row.dns || row.ip | uri %]">
[% row.dns || row.name || row.ip | html_entity %]</a></td>
<td>
[% IF row.location %]
<a href="[% search_device %]&q=[% row.location | uri %]&location=[% row.location | uri %]">
[% row.location | html_entity %]</a>
[% ELSE %]
[Not Set]
[% END %]
</td>
<td>[% row.contact | html_entity %]</td>
<td>[% row.vendor | html_entity %]</td>
<td>[% row.model | html_entity %]</td>
</tr>
[%END%]
</tbody>
</table>
</div>
</div>
</div>
</div>
[% 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 %]
<div class="accordion" id="accordion-networks">
[% count = 0 %]
[% FOREACH network IN graphs %]
[% count = count + 1 %]
[%# The largest is not an orphan, so skip %]
[% NEXT IF count == 1 %]
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" data-toggle="collapse" data-target="#collapse-[% count %]" href="#collapse-[% count %]">
<i class="icon-chevron-up"></i> &nbsp;
Orphaned Network: [% count - 1 | html_entity %] Size: [% network.size | html_entity %] Devices
</a>
</div>
<div id="collapse-[% count %]" class="accordion-body collapse">
<div class="accordion-inner">
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th>Device</th>
<th>Location</th>
<th>Contact</th>
<th>Vendor</th>
<th>Model</th>
</tr>
</thead>
<tbody>
[% FOREACH row IN network %]
<tr>
<td><a href="[% uri_for('/device') %]?tab=netmap&q=[% row.dns || row.ip | uri %]">
[% row.dns || row.name || row.ip | html_entity %]</a></td>
<td>
[% IF row.location %]
<a href="[% search_device %]&q=[% row.location | uri %]&location=[% row.location | uri %]">
[% row.location | html_entity %]</a>
[% ELSE %]
[Not Set]
[% END %]
</td>
<td>[% row.contact | html_entity %]</td>
<td>[% row.vendor | html_entity %]</td>
<td>[% row.model | html_entity %]</td>
</tr>
[% END %]
</tbody>
</table>
</div>
</div>
</div>
[% END %]
</div>
[% END %]
<script>
$('.accordion').on('show hide', function (n) {
$(n.target).siblings('.accordion-heading').find('.accordion-toggle i').toggleClass('icon-chevron-up icon-chevron-down');
});
</script>

View File

@@ -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 %]