#68 Devices orphaned by missing topology info report
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
[NEW FEATURES]
|
[NEW FEATURES]
|
||||||
|
|
||||||
* Support for Link Aggregation (port-channel, etherchannel, "trunking", etc)
|
* Support for Link Aggregation (port-channel, etherchannel, "trunking", etc)
|
||||||
|
* [#68] Devices orphaned by missing topology info report
|
||||||
|
|
||||||
[ENHANCEMENTS]
|
[ENHANCEMENTS]
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
54
Netdisco/lib/App/Netdisco/DB/Result/Virtual/UnDirEdgesAgg.pm
Normal file
54
Netdisco/lib/App/Netdisco/DB/Result/Virtual/UnDirEdgesAgg.pm
Normal 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;
|
||||||
@@ -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;
|
||||||
@@ -58,6 +58,7 @@ web_plugins:
|
|||||||
- AdminTask::PseudoDevice
|
- AdminTask::PseudoDevice
|
||||||
- AdminTask::SlowDevices
|
- AdminTask::SlowDevices
|
||||||
- AdminTask::UndiscoveredNeighbors
|
- AdminTask::UndiscoveredNeighbors
|
||||||
|
- AdminTask::OrphanedDevices
|
||||||
- AdminTask::UserLog
|
- AdminTask::UserLog
|
||||||
- AdminTask::Users
|
- AdminTask::Users
|
||||||
- Search::Device
|
- Search::Device
|
||||||
|
|||||||
105
Netdisco/share/views/ajax/admintask/orphaned.tt
Normal file
105
Netdisco/share/views/ajax/admintask/orphaned.tt
Normal 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>
|
||||||
|
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>
|
||||||
|
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>
|
||||||
51
Netdisco/share/views/ajax/admintask/orphaned_csv.tt
Normal file
51
Netdisco/share/views/ajax/admintask/orphaned_csv.tt
Normal 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 %]
|
||||||
Reference in New Issue
Block a user