Files
Oliver Gorwits 9eb537a4c1 #910 implement import of snmpwalk and more robust snapshot handling (#1086)
* initial work

* initial work

* initial work

* some fixes for time and Layer3 weird spec

* store again the snapshot after update for specific

* resolve the enums

* monkeypatch SNMP::translateObj to avoid hardware exception on macOS

* only save cache to db in the late phase worker

* no need to check for cache on transport, can just go ahead and try

* use database only for oidmap instead of netdisco-mibs

* rewrite device snapshot to gather loaded mib leafs only

* remove old walker code from snapshot worker

* allow snmp browser to work without snapshot

* only store snapshot leafs which the device responded on

* refactor to separate snapshot work from snmp transport work

* refactor to separate snapshot work from snmp transport work

* allow typeahead on MIB qualified leafs

* fixes for snmpwalk input after previous refactor

* add the extra stuff SNMP::Info device class uses into snapshot

* better width for snmp search box

* fix css for snmp options

* add spinner while snmp loading

* add spinner while snmp loading

* add spinner while snmp loading

* support SNMP::Info device class or named MIBs as extra on snapshot

* add final tidy and bug fix
2023-08-10 22:27:02 +01:00

190 lines
6.9 KiB
Perl
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package App::Netdisco::Web::Plugin::Device::SNMP;
use strict;
use warnings;
use Dancer qw(:syntax);
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Swagger;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::SNMP qw(%ALL_MUNGERS decode_and_munge);
use Module::Load ();
use Try::Tiny;
register_device_tab({ tag => 'snmp', label => 'SNMP',
render_if => sub { schema(vars->{'tenant'})->resultset('DeviceBrowser')->count() } });
get '/ajax/content/device/snmp' => require_login sub {
my $device = try { schema(vars->{'tenant'})->resultset('Device')
->search_for_device( param('q') ) }
or send_error('Bad Device', 404);
template 'ajax/device/snmp.tt', { device => $device->ip },
{ layout => 'noop' };
};
ajax '/ajax/data/device/:ip/snmptree/:base' => require_login sub {
my $device = try { schema(vars->{'tenant'})->resultset('Device')
->find( param('ip') ) }
or send_error('Bad Device', 404);
my $base = param('base');
$base =~ m/^\.1(\.\d+)*$/ or send_error('Bad OID Base', 404);
content_type 'application/json';
return to_json [{
text => 'No data for this device. You can request a snapshot in the Details tab.',
children => \0,
state => { disabled => \1 },
icon => 'icon-search',
}] unless $device->oids->count;
return to_json [{
text => 'No MIB data. Please run `~/bin/netdisco-do loadmibs`.',
children => \0,
state => { disabled => \1 },
icon => 'icon-search',
}] unless schema(vars->{'tenant'})->resultset('SNMPObject')->count();
my $items = _get_snmp_data($device->ip, $base);
to_json $items;
};
ajax '/ajax/data/snmp/typeahead' => require_login sub {
my $term = param('term') or return to_json [];
my $device = param('ip');
my $deviceonly = param('deviceonly');
my $table = ($deviceonly ? 'DeviceBrowser' : 'SNMPObject');
my @found = schema(vars->{'tenant'})->resultset($table)
->search({ -or => [ 'me.oid' => $term,
'me.oid' => { -like => ($term .'.%') },
'me.leaf' => { -ilike => ('%'. $term .'%') } ],
(($deviceonly and $device) ? (ip => $device) : ()), },
{ select => [
(($deviceonly and $device) ? \q{ oid_fields.mib || '::' || me.leaf }
: \q{ me.mib || '::' || me.leaf }),
],
as => ['qleaf'],
(($deviceonly and $device) ? (join => 'oid_fields') : ()),
rows => 25, order_by => 'me.oid_parts' })
->get_column('qleaf')->all;
return to_json [] unless scalar @found;
content_type 'application/json';
to_json [ sort @found ];
};
ajax '/ajax/data/snmp/nodesearch' => require_login sub {
my $to_match = param('str') or return to_json [];
my $partial = param('partial');
my $found = undef;
if ($partial) {
$found = schema(vars->{'tenant'})->resultset('SNMPObject')
->search({ -or => [ oid => $to_match,
oid => { -like => ($to_match .'.%') },
leaf => { -ilike => ($to_match .'%') } ] },
{ rows => 1, order_by => 'oid_parts' })->first;
}
else {
my ($mib, $leaf) = split m/::/, $to_match;
$found = schema(vars->{'tenant'})->resultset('SNMPObject')
->search({
(($mib and $leaf) ? (-and => [mib => $mib, leaf => $leaf])
: (-or => [oid => $to_match, leaf => { -ilike => $to_match }])),
},{ rows => 1, order_by => 'oid_parts' })->first;
}
return to_json [] unless $found;
$found = $found->oid;
$found =~ s/^\.1\.?//;
my @results = ('.1');
foreach my $part (split m/\./, $found) {
my $last = $results[-1];
push @results, "${last}.${part}";
}
content_type 'application/json';
to_json \@results;
};
ajax '/ajax/content/device/:ip/snmpnode/:oid' => require_login sub {
my $device = try { schema(vars->{'tenant'})->resultset('Device')
->find( param('ip') ) }
or send_error('Bad Device', 404);
my $oid = param('oid');
$oid =~ m/^\.1(\.\d+)*$/ or send_error('Bad OID', 404);
my $object = schema(vars->{'tenant'})->resultset('DeviceBrowser')
->with_snmp_object($device->ip)->find({ 'snmp_object.oid' => $oid })
or send_error('Bad OID', 404);
my $munge = (param('munge') and exists $ALL_MUNGERS{param('munge')})
? param('munge') : $object->munge;
my %data = (
$object->get_columns,
snmp_object => { $object->snmp_object->get_columns },
value => decode_and_munge( $munge, $object->value ),
);
template 'ajax/device/snmpnode.tt',
{ node => \%data, munge => $munge, mungers => [sort keys %ALL_MUNGERS] },
{ layout => 'noop' };
};
sub _get_snmp_data {
my ($ip, $base, $recurse) = @_;
my @parts = grep {length} split m/\./, $base;
my %meta = map { ('.'. join '.', @{$_->{oid_parts}}) => $_ }
schema(vars->{'tenant'})->resultset('Virtual::FilteredSNMPObject')
->search({}, { bind => [
$ip,
(scalar @parts + 1),
(scalar @parts + 1),
$base,
] })->hri->all;
my @items = map {{
id => $_,
mib => $meta{$_}->{mib}, # accessed via node.original.mib
leaf => $meta{$_}->{leaf}, # accessed via node.original.leaf
text => ($meta{$_}->{leaf} .' ('. $meta{$_}->{oid_parts}->[-1] .')'),
($meta{$_}->{browser} ? (icon => 'icon-folder-close text-info')
: (icon => 'icon-folder-close-alt muted')),
(scalar @{$meta{$_}->{index}}
? (icon => 'icon-th'.($meta{$_}->{browser} ? ' text-info' : ' muted')) : ()),
(($meta{$_}->{num_children} == 0 and ($meta{$_}->{type}
or $meta{$_}->{access} =~ m/^(?:read|write)/
or $meta{$_}->{oid_parts}->[-1] == 0))
? (icon => 'icon-leaf'.($meta{$_}->{browser} ? ' text-info' : ' muted')) : ()),
# jstree will async call to expand these, and while it's possible
# for us to prefetch by calling _get_snmp_data() and passing to
# children, it's much slower UX. async is better for search especially
children => ($meta{$_}->{num_children} ? \1 : \0),
# and set the display to open to show the single child
# but only if there is data below
state => { opened => (($meta{$_}->{browser} and $meta{$_}->{num_children} == 1) ? \1 : \0 ) },
}} sort {$meta{$a}->{oid_parts}->[-1] <=> $meta{$b}->{oid_parts}->[-1]} keys %meta;
return \@items;
}
true;