Feature to gather SNMP Walk, use as Pseudo Device, and Browse Objects
* fix anomalous name * add gather worker * fix encoding of binary storage * store results back to job * now parsing mbis report to translate * fix the broken report parser * rename gather to snapshot * implement walk code copied from SNMP::Info * can now bulkwalk and parse mibs report and store resolved walk in cache * add func/glob aliasing broken * better aliasing * implement aliasing from globals and funcs * fix regexp for matching netdisco-mibs report * fake cache entry for all ND2 methods called, add comments * also save to logs/snapshots/IP * add doc for netdisco-do * add is_pseudo column to device table * support for loading cache for pseudo devices * check for hrSystemUptime as well as sysUpTime for snmp connect * display pseudo devices with yellow pill for name * color all cells for layers for pseudo * no need to b64 encode binary data in scalars as we b64 whole thing after * tweaked uptime check * store snapshot to database instead of Job * expose snapshots in device details tab * small ux improvements on snap download * fixes for errors in subnet mask searching * hide snapshot management for pseudo devices * update to use new netdisco-mibs object cache * update for new format oids file * start of work on loading walk into db for browsing * store values and meta * add auto increment col and oid index to browser * start web plugin for browser * add virtual search for oid children * have all oid in separte table (60 seconds load on my laptop) * rename table and add relation * store oid as int array * fix sql for children * make jstree start working * working very slow tree expand * fix to work when first displaying tree * store both oid and oid_parts * simplify SQL to speed up (more complicated perl) * fix sql bug, add better index, prettify tree * render the snmp node detail * add node template, make scrollable, pretty print data values (insecure) * store munge hint * some dubious code to munge the data * make sure to filter by IP on device_browser * make safer the rendering of value data (but need to come back to key ordering) * fix sorting on object values * limit the opening of child nodes to keep response good and unclutter * factor out the munge and make safer * reject unknown mungers * show the munger and option (not working) to change * additional js for munge select * complete custom munge * change so that saving to database is only at CLI and on request * hide snmp tab if no browser rows in the db * add helpful message when no browser rows for the device * stub handler for search and add recurse control * working search * minor ui fixes * implement typeahead for leaf search * limit rows in typeahead * make sure device_browser is visited in delete and renumber * add requirements for this branch * update manifest * make sure node search and typeahead are restricted to current device only
This commit is contained in:
@@ -11,7 +11,7 @@ __PACKAGE__->load_namespaces(
|
||||
);
|
||||
|
||||
our # try to hide from kwalitee
|
||||
$VERSION = 66; # schema version used for upgrades, keep as integer
|
||||
$VERSION = 70; # schema version used for upgrades, keep as integer
|
||||
|
||||
use Path::Class;
|
||||
use File::ShareDir 'dist_dir';
|
||||
|
||||
@@ -81,6 +81,8 @@ __PACKAGE__->add_columns(
|
||||
{ data_type => "timestamp", is_nullable => 1 },
|
||||
"last_arpnip",
|
||||
{ data_type => "timestamp", is_nullable => 1 },
|
||||
"is_pseudo",
|
||||
{ data_type => "boolean", is_nullable => 0, default_value => \"false" },
|
||||
);
|
||||
__PACKAGE__->set_primary_key("ip");
|
||||
|
||||
@@ -172,6 +174,14 @@ Returns the set of power modules on this Device.
|
||||
|
||||
__PACKAGE__->has_many( power_modules => 'App::Netdisco::DB::Result::DevicePower', 'ip' );
|
||||
|
||||
=head2 oids
|
||||
|
||||
Returns the oids walked on this Device.
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->has_many( oids => 'App::Netdisco::DB::Result::DeviceBrowser', 'ip' );
|
||||
|
||||
=head2 port_vlans
|
||||
|
||||
Returns the set of VLANs known to be configured on Ports on this Device,
|
||||
@@ -256,6 +266,15 @@ Returns the row from the community string table, if one exists.
|
||||
__PACKAGE__->might_have(
|
||||
community => 'App::Netdisco::DB::Result::Community', 'ip');
|
||||
|
||||
=head2 snapshot
|
||||
|
||||
Returns the row from the snapshot table, if one exists.
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->might_have(
|
||||
snapshot => 'App::Netdisco::DB::Result::DeviceSnapshot', 'ip');
|
||||
|
||||
=head2 throughput
|
||||
|
||||
Returns a sum of speeds on all ports on the device.
|
||||
@@ -267,17 +286,6 @@ __PACKAGE__->has_one(
|
||||
|
||||
=head1 ADDITIONAL METHODS
|
||||
|
||||
=head2 is_pseudo
|
||||
|
||||
Returns true if the vendor of the device is "netdisco".
|
||||
|
||||
=cut
|
||||
|
||||
sub is_pseudo {
|
||||
my $device = shift;
|
||||
return (defined $device->vendor and $device->vendor eq 'netdisco');
|
||||
}
|
||||
|
||||
=head2 has_layer( $number )
|
||||
|
||||
Returns true if the device provided sysServices and supports the given layer.
|
||||
@@ -314,8 +322,12 @@ sub renumber {
|
||||
|
||||
# Community is not included as SNMP::test_connection will take care of it
|
||||
foreach my $set (qw/
|
||||
DeviceBrowser
|
||||
DeviceIp
|
||||
DeviceModule
|
||||
DevicePower
|
||||
DeviceSnapshot
|
||||
DeviceVlan
|
||||
DevicePort
|
||||
DevicePortLog
|
||||
DevicePortPower
|
||||
@@ -323,8 +335,6 @@ sub renumber {
|
||||
DevicePortSsid
|
||||
DevicePortVlan
|
||||
DevicePortWireless
|
||||
DevicePower
|
||||
DeviceVlan
|
||||
/) {
|
||||
$schema->resultset($set)
|
||||
->search({ip => $old_ip})
|
||||
@@ -353,6 +363,11 @@ sub renumber {
|
||||
->search({dev2 => $old_ip})
|
||||
->update({dev2 => $new_ip});
|
||||
|
||||
$schema->resultset('Admin')->search({
|
||||
device => $old_ip,
|
||||
status => { '-not_like' => 'queued-%' },
|
||||
})->delete;
|
||||
|
||||
$device->update({
|
||||
ip => $new_ip,
|
||||
dns => hostname_from_ip($new_ip),
|
||||
|
||||
50
lib/App/Netdisco/DB/Result/DeviceBrowser.pm
Normal file
50
lib/App/Netdisco/DB/Result/DeviceBrowser.pm
Normal file
@@ -0,0 +1,50 @@
|
||||
use utf8;
|
||||
package App::Netdisco::DB::Result::DeviceBrowser;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'App::Netdisco::DB::Result';
|
||||
__PACKAGE__->table("device_browser");
|
||||
__PACKAGE__->add_columns(
|
||||
"ip",
|
||||
{ data_type => "inet", is_nullable => 0 },
|
||||
"oid",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"oid_parts",
|
||||
{ data_type => "integer[]", is_nullable => 0 },
|
||||
"leaf",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"munge",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"value",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
);
|
||||
__PACKAGE__->set_primary_key("ip", "oid");
|
||||
|
||||
=head1 RELATIONSHIPS
|
||||
|
||||
=head2 snmp_object
|
||||
|
||||
Returns the SNMP Object table entry to which the given row is related. The
|
||||
idea is that you always get the SNMP Object row data even if the Device
|
||||
Browser table doesn't have any walked data.
|
||||
|
||||
However you probably want to use the C<snmp_object> method in the
|
||||
C<DeviceBrowser> ResultSet instead, so you can pass the IP address.
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to(
|
||||
snmp_object => 'App::Netdisco::DB::Result::SNMPObject',
|
||||
sub {
|
||||
my $args = shift;
|
||||
return {
|
||||
"$args->{self_alias}.oid" => { -ident => "$args->{foreign_alias}.oid" },
|
||||
"$args->{self_alias}.ip" => { '=' => \'?' },
|
||||
};
|
||||
},
|
||||
{ join_type => 'RIGHT' }
|
||||
);
|
||||
|
||||
1;
|
||||
27
lib/App/Netdisco/DB/Result/DeviceSnapshot.pm
Normal file
27
lib/App/Netdisco/DB/Result/DeviceSnapshot.pm
Normal file
@@ -0,0 +1,27 @@
|
||||
use utf8;
|
||||
package App::Netdisco::DB::Result::DeviceSnapshot;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'App::Netdisco::DB::Result';
|
||||
__PACKAGE__->table("device_snapshot");
|
||||
__PACKAGE__->add_columns(
|
||||
"ip",
|
||||
{ data_type => "inet", is_nullable => 0 },
|
||||
"cache",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
);
|
||||
__PACKAGE__->set_primary_key("ip");
|
||||
|
||||
=head1 RELATIONSHIPS
|
||||
|
||||
=head2 device
|
||||
|
||||
Returns the entry from the C<device> table on which this snapshot was created.
|
||||
|
||||
=cut
|
||||
|
||||
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
|
||||
|
||||
1;
|
||||
27
lib/App/Netdisco/DB/Result/SNMPObject.pm
Normal file
27
lib/App/Netdisco/DB/Result/SNMPObject.pm
Normal file
@@ -0,0 +1,27 @@
|
||||
use utf8;
|
||||
package App::Netdisco::DB::Result::SNMPObject;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'App::Netdisco::DB::Result';
|
||||
__PACKAGE__->table("snmp_object");
|
||||
__PACKAGE__->add_columns(
|
||||
"oid",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"oid_parts",
|
||||
{ data_type => "integer[]", is_nullable => 0 },
|
||||
"mib",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"leaf",
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
"type",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"access",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"index",
|
||||
{ data_type => "text[]", is_nullable => 1, default_value => \"'{}'::text[]" },
|
||||
);
|
||||
__PACKAGE__->set_primary_key("oid");
|
||||
|
||||
1;
|
||||
34
lib/App/Netdisco/DB/Result/Virtual/FilteredSNMPObject.pm
Normal file
34
lib/App/Netdisco/DB/Result/Virtual/FilteredSNMPObject.pm
Normal file
@@ -0,0 +1,34 @@
|
||||
use utf8;
|
||||
package App::Netdisco::DB::Result::Virtual::FilteredSNMPObject;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
|
||||
|
||||
__PACKAGE__->table("filtered_snmp_object");
|
||||
__PACKAGE__->result_source_instance->is_virtual(1);
|
||||
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
|
||||
|
||||
SELECT oid, oid_parts, mib, leaf, type, access, index
|
||||
FROM snmp_object
|
||||
WHERE oid LIKE ?::text || '.%'
|
||||
AND oid_parts[?] = ANY (?)
|
||||
AND array_length(oid_parts,1) = ?
|
||||
|
||||
ENDSQL
|
||||
);
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
'oid' => { data_type => 'text' },
|
||||
'oid_parts' => { data_type => 'integer[]' },
|
||||
'mib' => { data_type => 'text' },
|
||||
'leaf' => { data_type => 'text' },
|
||||
'type' => { data_type => 'text' },
|
||||
'access' => { data_type => 'text' },
|
||||
'index' => { data_type => 'text[]' },
|
||||
);
|
||||
|
||||
1;
|
||||
36
lib/App/Netdisco/DB/Result/Virtual/OidChildren.pm
Normal file
36
lib/App/Netdisco/DB/Result/Virtual/OidChildren.pm
Normal file
@@ -0,0 +1,36 @@
|
||||
use utf8;
|
||||
package App::Netdisco::DB::Result::Virtual::OidChildren;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
|
||||
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
|
||||
|
||||
__PACKAGE__->table("oid_children");
|
||||
__PACKAGE__->result_source_instance->is_virtual(1);
|
||||
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
|
||||
|
||||
SELECT DISTINCT(db.oid_parts[?]) AS part, count(distinct(db2.oid_parts[?])) as children
|
||||
FROM device_browser db
|
||||
|
||||
LEFT JOIN device_browser db2
|
||||
ON (db2.oid LIKE ? || '.%'
|
||||
AND db2.oid_parts[?] = db.oid_parts[?]
|
||||
AND db2.ip = db.ip)
|
||||
|
||||
WHERE db.ip = ?
|
||||
AND db.oid LIKE ? || '.%'
|
||||
|
||||
GROUP BY db.oid_parts
|
||||
|
||||
ENDSQL
|
||||
);
|
||||
|
||||
__PACKAGE__->add_columns(
|
||||
'part' => { data_type => 'integer' },
|
||||
'children' => { data_type => 'integer' },
|
||||
);
|
||||
|
||||
1;
|
||||
@@ -704,11 +704,13 @@ sub delete {
|
||||
$ip ||= 'netdisco';
|
||||
|
||||
foreach my $set (qw/
|
||||
DeviceIp
|
||||
DeviceVlan
|
||||
DevicePower
|
||||
DeviceModule
|
||||
Community
|
||||
DeviceBrowser
|
||||
DeviceIp
|
||||
DeviceModule
|
||||
DevicePower
|
||||
DeviceSnapshot
|
||||
DeviceVlan
|
||||
/) {
|
||||
my $gone = $schema->resultset($set)->search(
|
||||
{ ip => { '-in' => $devices->as_query } },
|
||||
@@ -746,3 +748,35 @@ sub delete {
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
list of tables in the db that use the device:
|
||||
|
||||
# use 'ip' as PK
|
||||
community
|
||||
device_browser
|
||||
device_ip
|
||||
device_module
|
||||
device_power
|
||||
device_snapshot
|
||||
device_vlan
|
||||
|
||||
# use 'device' as PK
|
||||
admin
|
||||
device_skip
|
||||
topology
|
||||
|
||||
# special to let nodes be kept
|
||||
device_port
|
||||
|
||||
# defer to port resultset class
|
||||
device_port_power
|
||||
device_port_properties
|
||||
device_port_ssid
|
||||
device_port_vlan
|
||||
device_port_wireless
|
||||
device_port_log
|
||||
|
||||
# dbic does this one itself
|
||||
device
|
||||
|
||||
|
||||
28
lib/App/Netdisco/DB/ResultSet/DeviceBrowser.pm
Normal file
28
lib/App/Netdisco/DB/ResultSet/DeviceBrowser.pm
Normal file
@@ -0,0 +1,28 @@
|
||||
package App::Netdisco::DB::ResultSet::DeviceBrowser;
|
||||
use base 'App::Netdisco::DB::ResultSet';
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
=head1 ADDITIONAL METHODS
|
||||
|
||||
=head2 with_snmp_object( $ip )
|
||||
|
||||
Returns a correlated subquery for the set of C<snmp_object> entry for
|
||||
the walked data row.
|
||||
|
||||
=cut
|
||||
|
||||
sub with_snmp_object {
|
||||
my ($rs, $ip) = @_;
|
||||
$ip ||= '255.255.255.255';
|
||||
|
||||
return $rs->search(undef,{
|
||||
# NOTE: bind param list order is significant
|
||||
join => ['snmp_object'],
|
||||
bind => [$ip],
|
||||
prefetch => 'snmp_object',
|
||||
});
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -254,9 +254,9 @@ sub delete {
|
||||
foreach my $set (qw/
|
||||
DevicePortPower
|
||||
DevicePortProperties
|
||||
DevicePortSsid
|
||||
DevicePortVlan
|
||||
DevicePortWireless
|
||||
DevicePortSsid
|
||||
/) {
|
||||
my $gone = $schema->resultset($set)->search(
|
||||
{ ip => { '-in' => $ports->as_query }},
|
||||
|
||||
@@ -10,7 +10,12 @@ use App::Netdisco::Util::Permission ':all';
|
||||
use SNMP::Info;
|
||||
use Try::Tiny;
|
||||
use Module::Load ();
|
||||
use Storable 'thaw';
|
||||
use File::Slurper 'read_text';
|
||||
use MIME::Base64 'decode_base64';
|
||||
use Path::Class 'dir';
|
||||
use File::Path 'make_path';
|
||||
use File::Spec::Functions qw(catdir catfile);
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
use List::Util qw/pairkeys pairfirst/;
|
||||
|
||||
@@ -58,7 +63,9 @@ Returns C<undef> if the connection fails.
|
||||
sub reader_for {
|
||||
my ($class, $ip, $useclass) = @_;
|
||||
my $device = get_device($ip) or return undef;
|
||||
return undef if $device->in_storage and $device->is_pseudo;
|
||||
|
||||
my $pseudo_cache = catfile( catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'logs', 'snapshots'), $device->ip );
|
||||
return undef if $device->in_storage and $device->is_pseudo and ! -f $pseudo_cache;
|
||||
|
||||
my $readers = $class->instance->readers or return undef;
|
||||
return $readers->{$device->ip} if exists $readers->{$device->ip};
|
||||
@@ -107,6 +114,7 @@ Returns C<undef> if the connection fails.
|
||||
sub writer_for {
|
||||
my ($class, $ip, $useclass) = @_;
|
||||
my $device = get_device($ip) or return undef;
|
||||
|
||||
return undef if $device->in_storage and $device->is_pseudo;
|
||||
|
||||
my $writers = $class->instance->writers or return undef;
|
||||
@@ -155,6 +163,13 @@ sub _snmp_connect_generic {
|
||||
$snmp_args{BulkWalk} = 0;
|
||||
}
|
||||
|
||||
# support for offline cache
|
||||
if ($device->is_pseudo) {
|
||||
my $pseudo_cache = catfile( catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'logs', 'snapshots'), $device->ip );
|
||||
$snmp_args{Cache} = thaw( decode_base64( read_text($pseudo_cache) ) );
|
||||
$snmp_args{Offline} = 1;
|
||||
}
|
||||
|
||||
# get the community string(s)
|
||||
my @communities = get_communities($device, $mode);
|
||||
|
||||
@@ -242,7 +257,7 @@ sub _try_read {
|
||||
|
||||
return undef unless (
|
||||
(not defined $info->error)
|
||||
and defined $info->uptime
|
||||
and (defined $info->uptime or defined $info->hrSystemUptime or defined $info->sysUpTime)
|
||||
and ($info->layers or $info->description)
|
||||
and $info->class
|
||||
);
|
||||
|
||||
@@ -4,6 +4,9 @@ use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/;
|
||||
|
||||
use File::Spec::Functions qw(catdir catfile);
|
||||
use File::Path 'make_path';
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/
|
||||
@@ -158,7 +161,7 @@ If C<$device_type> is also given, then C<discover_no_type> will be checked.
|
||||
Also respects C<discover_phones> and C<discover_waps> if either are set to
|
||||
false.
|
||||
|
||||
Also checks if the device is a pseudo device (vendor is C<netdisco>).
|
||||
Also checks if the device is a pseudo device and no offline cache exists.
|
||||
|
||||
Returns false if the host is not permitted to discover the target device.
|
||||
|
||||
@@ -170,8 +173,9 @@ sub is_discoverable {
|
||||
$remote_type ||= '';
|
||||
$remote_cap ||= [];
|
||||
|
||||
return _bail_msg("is_discoverable: $device is pseudo-device")
|
||||
if $device->is_pseudo;
|
||||
my $pseudo_cache = catfile( catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'logs', 'snapshots'), $device->ip );
|
||||
return _bail_msg("is_discoverable: $device is pseudo-device without offline cache")
|
||||
if $device->is_pseudo and ! -f $pseudo_cache;
|
||||
|
||||
return _bail_msg("is_discoverable: $device matches wap_platforms but discover_waps is not enabled")
|
||||
if ((not setting('discover_waps')) and
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package App::Netdisco::Util::SNMP;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer qw/:syntax :script !to_json !from_json/;
|
||||
use App::Netdisco::Util::DeviceAuth 'get_external_credentials';
|
||||
|
||||
use MIME::Base64 'decode_base64';
|
||||
use Storable 'thaw';
|
||||
use JSON::PP;
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/ get_communities snmp_comm_reindex /;
|
||||
our @EXPORT_OK = qw/
|
||||
get_communities
|
||||
snmp_comm_reindex
|
||||
sortable_oid
|
||||
decode_and_munge
|
||||
%ALL_MUNGERS
|
||||
/;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
@@ -21,6 +31,24 @@ subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 sortable_oid( $oid, $seglen? )
|
||||
|
||||
Take an OID and return a version of it which is sortable using C<cmp>
|
||||
operator. Works by zero-padding the numeric parts all to be length
|
||||
C<< $seglen >>, which defaults to 6.
|
||||
|
||||
=cut
|
||||
|
||||
# take oid and make comparable
|
||||
sub sortable_oid {
|
||||
my ($oid, $seglen) = @_;
|
||||
$seglen ||= 6;
|
||||
return $oid if $oid !~ m/^[0-9.]+$/;
|
||||
$oid =~ s/^(\.)//; my $leading = $1;
|
||||
$oid = join '.', map { sprintf("\%0${seglen}d", $_) } (split m/\./, $oid);
|
||||
return (($leading || '') . $oid);
|
||||
}
|
||||
|
||||
=head2 get_communities( $device, $mode )
|
||||
|
||||
Takes the current C<device_auth> setting and pushes onto the front of the list
|
||||
@@ -130,4 +158,93 @@ sub snmp_comm_reindex {
|
||||
return $snmp;
|
||||
}
|
||||
|
||||
our %ALL_MUNGERS = (
|
||||
'SNMP::Info::munge_speed' => \&SNMP::Info::munge_speed,
|
||||
'SNMP::Info::munge_highspeed' => \&SNMP::Info::munge_highspeed,
|
||||
'SNMP::Info::munge_ip' => \&SNMP::Info::munge_ip,
|
||||
'SNMP::Info::munge_mac' => \&SNMP::Info::munge_mac,
|
||||
'SNMP::Info::munge_prio_mac' => \&SNMP::Info::munge_prio_mac,
|
||||
'SNMP::Info::munge_prio_port' => \&SNMP::Info::munge_prio_port,
|
||||
'SNMP::Info::munge_octet2hex' => \&SNMP::Info::munge_octet2hex,
|
||||
'SNMP::Info::munge_dec2bin' => \&SNMP::Info::munge_dec2bin,
|
||||
'SNMP::Info::munge_bits' => \&SNMP::Info::munge_bits,
|
||||
'SNMP::Info::munge_counter64' => \&SNMP::Info::munge_counter64,
|
||||
'SNMP::Info::munge_i_up' => \&SNMP::Info::munge_i_up,
|
||||
'SNMP::Info::munge_port_list' => \&SNMP::Info::munge_port_list,
|
||||
'SNMP::Info::munge_null' => \&SNMP::Info::munge_null,
|
||||
'SNMP::Info::munge_e_type' => \&SNMP::Info::munge_e_type,
|
||||
'SNMP::Info::Airespace::munge_64bits' => \&SNMP::Info::Airespace::munge_64bits,
|
||||
'SNMP::Info::CDP::munge_power' => \&SNMP::Info::CDP::munge_power,
|
||||
'SNMP::Info::CiscoAgg::munge_port_ifindex' => \&SNMP::Info::CiscoAgg::munge_port_ifindex,
|
||||
'SNMP::Info::CiscoPortSecurity::munge_pae_capabilities' => \&SNMP::Info::CiscoPortSecurity::munge_pae_capabilities,
|
||||
'SNMP::Info::CiscoStack::munge_port_status' => \&SNMP::Info::CiscoStack::munge_port_status,
|
||||
'SNMP::Info::EtherLike::munge_el_duplex' => \&SNMP::Info::EtherLike::munge_el_duplex,
|
||||
'SNMP::Info::IPv6::munge_physaddr' => \&SNMP::Info::IPv6::munge_physaddr,
|
||||
'SNMP::Info::Layer2::Airespace::munge_cd11n_ch_bw' => \&SNMP::Info::Layer2::Airespace::munge_cd11n_ch_bw,
|
||||
'SNMP::Info::Layer2::Airespace::munge_cd11_proto' => \&SNMP::Info::Layer2::Airespace::munge_cd11_proto,
|
||||
'SNMP::Info::Layer2::Airespace::munge_cd11_rateset' => \&SNMP::Info::Layer2::Airespace::munge_cd11_rateset,
|
||||
'SNMP::Info::Layer2::Aironet::munge_cd11_txrate' => \&SNMP::Info::Layer2::Aironet::munge_cd11_txrate,
|
||||
'SNMP::Info::Layer2::HP::munge_hp_c_id' => \&SNMP::Info::Layer2::HP::munge_hp_c_id,
|
||||
'SNMP::Info::Layer2::Nexans::munge_i_duplex' => \&SNMP::Info::Layer2::Nexans::munge_i_duplex,
|
||||
'SNMP::Info::Layer2::Nexans::munge_i_duplex_admin' => \&SNMP::Info::Layer2::Nexans::munge_i_duplex_admin,
|
||||
'SNMP::Info::Layer3::Altiga::munge_alarm' => \&SNMP::Info::Layer3::Altiga::munge_alarm,
|
||||
'SNMP::Info::Layer3::Aruba::munge_aruba_fqln' => \&SNMP::Info::Layer3::Aruba::munge_aruba_fqln,
|
||||
'SNMP::Info::Layer3::BayRS::munge_hw_rev' => \&SNMP::Info::Layer3::BayRS::munge_hw_rev,
|
||||
'SNMP::Info::Layer3::BayRS::munge_wf_serial' => \&SNMP::Info::Layer3::BayRS::munge_wf_serial,
|
||||
'SNMP::Info::Layer3::Extreme::munge_true_ok' => \&SNMP::Info::Layer3::Extreme::munge_true_ok,
|
||||
'SNMP::Info::Layer3::Extreme::munge_power_stat' => \&SNMP::Info::Layer3::Extreme::munge_power_stat,
|
||||
'SNMP::Info::Layer3::Huawei::munge_hw_peth_admin' => \&SNMP::Info::Layer3::Huawei::munge_hw_peth_admin,
|
||||
'SNMP::Info::Layer3::Huawei::munge_hw_peth_power' => \&SNMP::Info::Layer3::Huawei::munge_hw_peth_power,
|
||||
'SNMP::Info::Layer3::Huawei::munge_hw_peth_class' => \&SNMP::Info::Layer3::Huawei::munge_hw_peth_class,
|
||||
'SNMP::Info::Layer3::Huawei::munge_hw_peth_status' => \&SNMP::Info::Layer3::Huawei::munge_hw_peth_status,
|
||||
'SNMP::Info::Layer3::Timetra::munge_tmnx_state' => \&SNMP::Info::Layer3::Timetra::munge_tmnx_state,
|
||||
'SNMP::Info::Layer3::Timetra::munge_tmnx_e_class' => \&SNMP::Info::Layer3::Timetra::munge_tmnx_e_class,
|
||||
'SNMP::Info::Layer3::Timetra::munge_tmnx_e_swver' => \&SNMP::Info::Layer3::Timetra::munge_tmnx_e_swver,
|
||||
'SNMP::Info::MAU::munge_int2bin' => \&SNMP::Info::MAU::munge_int2bin,
|
||||
'SNMP::Info::NortelStack::munge_ns_grp_type' => \&SNMP::Info::NortelStack::munge_ns_grp_type,
|
||||
);
|
||||
|
||||
=head2 decode_and_munge( $method, $data )
|
||||
|
||||
Takes some data from L<SNMP::Info> cache that has been Base64 encoded
|
||||
and frozen with Storable, decodes it and then munge to handle data format,
|
||||
before finally pretty render in JSON format.
|
||||
|
||||
=cut
|
||||
|
||||
sub get_code_info { return ($_[0]) =~ m/^(.+)::(.*?)$/ }
|
||||
sub sub_name { return (get_code_info $_[0])[1] }
|
||||
sub class_name { return (get_code_info $_[0])[0] }
|
||||
|
||||
sub decode_and_munge {
|
||||
my ($munger, $encoded) = @_;
|
||||
return undef unless defined $encoded and length $encoded;
|
||||
|
||||
my $coder = JSON::PP->new->utf8->pretty->allow_nonref->allow_unknown->canonical;
|
||||
$coder->sort_by( sub { sortable_oid($JSON::PP::a) cmp sortable_oid($JSON::PP::b) } );
|
||||
|
||||
my $data = (@{ thaw( decode_base64( $encoded ) ) })[0];
|
||||
return $coder->encode( $data )
|
||||
unless $munger and exists $ALL_MUNGERS{$munger};
|
||||
|
||||
my $sub = sub_name($munger);
|
||||
my $class = class_name($munger);
|
||||
Module::Load::load $class;
|
||||
|
||||
if (ref {} eq ref $data) {
|
||||
my %munged;
|
||||
foreach my $key ( keys %$data ) {
|
||||
my $value = $data->{$key};
|
||||
next unless defined $value;
|
||||
$munged{$key} = $ALL_MUNGERS{$munger}->($value);
|
||||
}
|
||||
return $coder->encode( \%munged );
|
||||
}
|
||||
else {
|
||||
return unless $data;
|
||||
return $coder->encode( $ALL_MUNGERS{$munger}->($data) );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
true;
|
||||
|
||||
@@ -58,6 +58,31 @@ ajax qr{/ajax/control/admin/(?:\w+/)?delete} => require_role setting('defanged_a
|
||||
);
|
||||
};
|
||||
|
||||
ajax "/ajax/control/admin/snapshot_req" => require_role admin => sub {
|
||||
my $device = NetAddr::IP->new(param('device'));
|
||||
send_error('Bad device', 400)
|
||||
if ! $device or $device->addr eq '0.0.0.0';
|
||||
|
||||
add_job('snapshot', $device->addr) or send_error('Bad device', 400);
|
||||
};
|
||||
|
||||
get "/ajax/content/admin/snapshot_get" => require_role admin => sub {
|
||||
my $device = NetAddr::IP->new(param('device'));
|
||||
send_error('Bad device', 400)
|
||||
if ! $device or $device->addr eq '0.0.0.0';
|
||||
|
||||
my $content = schema('netdisco')->resultset('DeviceSnapshot')->find($device->addr)->cache;
|
||||
send_file( \$content, content_type => 'text/plain', filename => ($device->addr .'-snapshot.txt') );
|
||||
};
|
||||
|
||||
ajax "/ajax/control/admin/snapshot_del" => require_role setting('defanged_admin') => sub {
|
||||
my $device = NetAddr::IP->new(param('device'));
|
||||
send_error('Bad device', 400)
|
||||
if ! $device or $device->addr eq '0.0.0.0';
|
||||
|
||||
schema('netdisco')->resultset('DeviceSnapshot')->find($device->addr)->delete;
|
||||
};
|
||||
|
||||
get '/admin/*' => require_role admin => sub {
|
||||
my ($tag) = splat;
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ get '/device' => require_login sub {
|
||||
|
||||
params->{'tab'} ||= 'details';
|
||||
template 'device', {
|
||||
is_pseudo => $first->is_pseudo,
|
||||
display_name => ($others ? $first->ip : ($first->dns || $first->ip)),
|
||||
lgroup_list => [ schema('netdisco')->resultset('Device')->get_distinct_col('location') ],
|
||||
hgroup_list => setting('host_group_displaynames'),
|
||||
|
||||
@@ -170,6 +170,7 @@ register 'register_search_tab' => sub {
|
||||
|
||||
register 'register_device_tab' => sub {
|
||||
my ($self, $config) = plugin_args(@_);
|
||||
$config->{render_if} ||= sub { true };
|
||||
_register_tab('device', $config);
|
||||
};
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ ajax '/ajax/control/admin/pseudodevice/add' => require_role admin => sub {
|
||||
vendor => 'netdisco',
|
||||
layers => param('layers'),
|
||||
last_discover => \'now()',
|
||||
is_pseudo => \'true',
|
||||
});
|
||||
return unless $device;
|
||||
|
||||
@@ -96,7 +97,7 @@ ajax '/ajax/control/admin/pseudodevice/update' => require_role admin => sub {
|
||||
ajax '/ajax/content/admin/pseudodevice' => require_role admin => sub {
|
||||
my $set = schema('netdisco')->resultset('Device')
|
||||
->search(
|
||||
{vendor => 'netdisco'},
|
||||
{-bool => 'is_pseudo'},
|
||||
{order_by => { -desc => 'last_discover' }},
|
||||
)->with_port_count;
|
||||
|
||||
|
||||
@@ -17,9 +17,15 @@ ajax '/ajax/content/device/details' => require_login sub {
|
||||
|
||||
my @results
|
||||
= schema('netdisco')->resultset('Device')
|
||||
->search( { 'me.ip' => $device->ip } )->with_times()
|
||||
->search({ 'me.ip' => $device->ip },
|
||||
{
|
||||
'+select' => ['snapshot.ip'],
|
||||
'+as' => ['has_snapshot'],
|
||||
join => 'snapshot',
|
||||
},
|
||||
)->with_times()
|
||||
->hri->all;
|
||||
|
||||
|
||||
my @power
|
||||
= schema('netdisco')->resultset('DevicePower')
|
||||
->search( { 'me.ip' => $device->ip } )->with_poestats->hri->all;
|
||||
|
||||
175
lib/App/Netdisco/Web/Plugin/Device/SNMP.pm
Normal file
175
lib/App/Netdisco/Web/Plugin/Device/SNMP.pm
Normal file
@@ -0,0 +1,175 @@
|
||||
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('netdisco')->resultset('DeviceBrowser')->count() } });
|
||||
|
||||
get '/ajax/content/device/snmp' => require_login sub {
|
||||
my $device = try { schema('netdisco')->resultset('Device')
|
||||
->find( param('ip') ) }
|
||||
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('netdisco')->resultset('Device')
|
||||
->find( param('ip') ) }
|
||||
or send_error('Bad Device', 404);
|
||||
|
||||
my $recurse = ((param('recurse') and param('recurse') eq 'on') ? 0 : 1);
|
||||
my $base = param('base');
|
||||
$base =~ m/^\.1\.3\.6\.1(\.\d+)*$/ or send_error('Bad OID Base', 404);
|
||||
|
||||
my $items = _get_snmp_data($device->ip, $base, $recurse);
|
||||
|
||||
content_type 'application/json';
|
||||
to_json $items;
|
||||
};
|
||||
|
||||
ajax '/ajax/data/device/:ip/typeahead' => require_login sub {
|
||||
my $device = try { schema('netdisco')->resultset('Device')
|
||||
->find( param('ip') ) }
|
||||
or send_error('Bad Device', 404);
|
||||
|
||||
my $term = param('term') or return to_json [];
|
||||
$term = '%'. $term .'%';
|
||||
|
||||
my @found = schema('netdisco')->resultset('DeviceBrowser')
|
||||
->search({ leaf => { -ilike => $term }, ip => $device->ip },
|
||||
{ rows => 25, columns => 'leaf' })
|
||||
->get_column('leaf')->all;
|
||||
return to_json [] unless scalar @found;
|
||||
|
||||
content_type 'application/json';
|
||||
to_json [ sort @found ];
|
||||
};
|
||||
|
||||
ajax '/ajax/data/device/:ip/snmpnodesearch' => require_login sub {
|
||||
my $device = try { schema('netdisco')->resultset('Device')
|
||||
->find( param('ip') ) }
|
||||
or send_error('Bad Device', 404);
|
||||
|
||||
my $to_match = param('str');
|
||||
my $partial = param('partial');
|
||||
my $excludeself = param('excludeself');
|
||||
|
||||
return to_json [] unless $to_match or length($to_match);
|
||||
$to_match = $to_match . '%' if $partial;
|
||||
my $found = undef;
|
||||
|
||||
my $op = ($partial ? '-ilike' : '=');
|
||||
$found = schema('netdisco')->resultset('DeviceBrowser')
|
||||
->search({ -or => [ oid => { $op => $to_match }, leaf => { $op => $to_match } ], ip => $device->ip },
|
||||
{ rows => 1, order_by => 'oid_parts' })->first;
|
||||
|
||||
return to_json [] unless $found;
|
||||
|
||||
$found = $found->oid;
|
||||
$found =~ s/^\.1\.3\.6\.1\.?//;
|
||||
my @results = ('.1.3.6.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('netdisco')->resultset('Device')
|
||||
->find( param('ip') ) }
|
||||
or send_error('Bad Device', 404);
|
||||
|
||||
my $oid = param('oid');
|
||||
$oid =~ m/^\.1\.3\.6\.1(\.\d+)*$/ or send_error('Bad OID', 404);
|
||||
|
||||
my $object = schema('netdisco')->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;
|
||||
++$recurse;
|
||||
|
||||
my %kids = map { ($base .'.'. $_->{part}) => $_ }
|
||||
schema('netdisco')->resultset('Virtual::OidChildren')
|
||||
->search({}, { bind => [
|
||||
(scalar @parts + 1),
|
||||
(scalar @parts + 2),
|
||||
$base,
|
||||
(scalar @parts + 1),
|
||||
(scalar @parts + 1),
|
||||
$ip,
|
||||
$base,
|
||||
] })->hri->all;
|
||||
|
||||
return [{
|
||||
text => 'No SNMP data for this device.',
|
||||
children => \0,
|
||||
state => { disabled => \1 },
|
||||
icon => 'icon-search',
|
||||
}] unless scalar keys %kids;
|
||||
|
||||
my %meta = map { ('.'. join '.', @{$_->{oid_parts}}) => $_ }
|
||||
schema('netdisco')->resultset('Virtual::FilteredSNMPObject')
|
||||
->search({}, { bind => [
|
||||
$base,
|
||||
(scalar @parts + 1),
|
||||
[[ map {$_->{part}} values %kids ]],
|
||||
(scalar @parts + 1),
|
||||
] })->hri->all;
|
||||
|
||||
my @items = map {{
|
||||
id => $_,
|
||||
text => ($meta{$_}->{leaf} .' ('. $kids{$_}->{part} .')'),
|
||||
|
||||
# for nodes with only one child, recurse to prefetch...
|
||||
children => ( ($recurse < 2 and $kids{$_}->{children} == 1)
|
||||
? _get_snmp_data($ip, ("${base}.". $kids{$_}->{part}), $recurse)
|
||||
: ($kids{$_}->{children} ? \1 : \0)),
|
||||
|
||||
# and set the display to open to show the single child
|
||||
state => { opened => ( ($recurse < 2 and $kids{$_}->{children} == 1)
|
||||
? \1
|
||||
: \0 ) },
|
||||
|
||||
($kids{$_}->{children} ? () : (icon => 'icon-leaf')),
|
||||
(scalar @{$meta{$_}->{index}} ? (icon => 'icon-th') : ()),
|
||||
}} sort {$kids{$a}->{part} <=> $kids{$b}->{part}} keys %kids;
|
||||
|
||||
return \@items;
|
||||
}
|
||||
|
||||
true;
|
||||
@@ -38,14 +38,14 @@ sub gather_subnets {
|
||||
|
||||
my $ip_netmask = $snmp->ip_netmask;
|
||||
foreach my $entry (keys %$ip_netmask) {
|
||||
my $ip = NetAddr::IP::Lite->new($entry);
|
||||
my $ip = NetAddr::IP::Lite->new($entry) or next;
|
||||
my $addr = $ip->addr;
|
||||
|
||||
next if $addr eq '0.0.0.0';
|
||||
next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__');
|
||||
next if setting('ignore_private_nets') and $ip->is_rfc1918;
|
||||
|
||||
my $netmask = $ip_netmask->{$addr};
|
||||
my $netmask = $ip_netmask->{$addr} || $ip->bits();
|
||||
next if $netmask eq '255.255.255.255' or $netmask eq '0.0.0.0';
|
||||
|
||||
my $cidr = NetAddr::IP::Lite->new($addr, $netmask)->network->cidr;
|
||||
|
||||
@@ -61,7 +61,7 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub {
|
||||
$device->set_column( last_discover => \'now()' );
|
||||
|
||||
# protection for failed SNMP gather
|
||||
if ($device->in_storage) {
|
||||
if ($device->in_storage and not $device->is_pseudo) {
|
||||
my $ip = $device->ip;
|
||||
my $protect = setting('snmp_field_protection')->{'device'} || {};
|
||||
my %dirty = $device->get_dirty_columns;
|
||||
@@ -199,7 +199,7 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub {
|
||||
my $agg_ports = $snmp->agg_ports;
|
||||
|
||||
# clear the cached uptime and get a new one
|
||||
my $dev_uptime = $snmp->load_uptime;
|
||||
my $dev_uptime = ($device->is_pseudo ? $snmp->uptime : $snmp->load_uptime);
|
||||
if (!defined $dev_uptime) {
|
||||
error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!',
|
||||
$device->ip;
|
||||
|
||||
50
lib/App/Netdisco/Worker/Plugin/LoadMIBs.pm
Normal file
50
lib/App/Netdisco/Worker/Plugin/LoadMIBs.pm
Normal file
@@ -0,0 +1,50 @@
|
||||
package App::Netdisco::Worker::Plugin::LoadMIBs;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use App::Netdisco::Worker::Plugin;
|
||||
use aliased 'App::Netdisco::Worker::Status';
|
||||
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use File::Spec::Functions qw(catdir catfile);
|
||||
use File::Slurper qw(read_lines write_text);
|
||||
# use DDP;
|
||||
|
||||
register_worker({ phase => 'main' }, sub {
|
||||
my ($job, $workerconf) = @_;
|
||||
|
||||
debug "loadmibs - loading netdisco-mibs object cache";
|
||||
|
||||
my $home = (setting('mibhome') || catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'netdisco-mibs'));
|
||||
my @report = read_lines(catfile($home, qw(EXTRAS reports all_oids)), 'latin-1');
|
||||
|
||||
my @browser = ();
|
||||
foreach my $line (@report) {
|
||||
my ($oid, $qual_leaf, $type, $access, $index) = split m/,/, $line;
|
||||
next unless defined $oid and defined $qual_leaf;
|
||||
my ($mib, $leaf) = split m/::/, $qual_leaf;
|
||||
push @browser, {
|
||||
oid => $oid,
|
||||
oid_parts => [ grep {length} (split m/\./, $oid) ],
|
||||
mib => $mib,
|
||||
leaf => $leaf,
|
||||
type => $type,
|
||||
access => $access,
|
||||
index => [($index ? (split m/:/, $index) : ())],
|
||||
};
|
||||
}
|
||||
|
||||
debug sprintf "loadmibs - loaded %d objects from netdisco-mibs",
|
||||
scalar @browser;
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $gone = schema('netdisco')->resultset('SNMPObject')->delete;
|
||||
debug sprintf ' loadmibs - removed %d oids', $gone;
|
||||
schema('netdisco')->resultset('SNMPObject')->populate(\@browser);
|
||||
debug sprintf ' loadmibs - added %d new oids', scalar @browser;
|
||||
});
|
||||
|
||||
return Status->done('Loaded MIBs');
|
||||
});
|
||||
|
||||
true;
|
||||
@@ -28,8 +28,8 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub {
|
||||
}
|
||||
}
|
||||
|
||||
my $i = App::Netdisco::Transport::SNMP->reader_for($device, $class);
|
||||
my $result = sub { eval { $i->$extra($port) } || undef };
|
||||
my $snmp = App::Netdisco::Transport::SNMP->reader_for($device, $class);
|
||||
my $result = sub { eval { $snmp->$extra($port) } || undef };
|
||||
Data::Printer::p( $result->() );
|
||||
|
||||
return Status->done(
|
||||
|
||||
433
lib/App/Netdisco/Worker/Plugin/Snapshot.pm
Normal file
433
lib/App/Netdisco/Worker/Plugin/Snapshot.pm
Normal file
@@ -0,0 +1,433 @@
|
||||
package App::Netdisco::Worker::Plugin::Snapshot;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use App::Netdisco::Worker::Plugin;
|
||||
use aliased 'App::Netdisco::Worker::Status';
|
||||
|
||||
use App::Netdisco::Transport::SNMP;
|
||||
use App::Netdisco::Util::SNMP 'sortable_oid';
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use File::Spec::Functions qw(catdir catfile);
|
||||
use MIME::Base64 'encode_base64';
|
||||
use File::Slurper qw(read_lines write_text);
|
||||
use File::Path 'make_path';
|
||||
use Sub::Util 'subname';
|
||||
use Storable qw(dclone nfreeze);
|
||||
# use DDP;
|
||||
|
||||
register_worker({ phase => 'check' }, sub {
|
||||
return Status->error('Missing device (-d).')
|
||||
unless defined shift->device;
|
||||
return Status->done('Snapshot is able to run');
|
||||
});
|
||||
|
||||
register_worker({ phase => 'main', driver => 'snmp' }, sub {
|
||||
my ($job, $workerconf) = @_;
|
||||
my $device = $job->device;
|
||||
my $save_file = $job->port;
|
||||
my $save_db = $job->extra;
|
||||
|
||||
# needed to avoid $var being returned with leafname and breaking loop checks
|
||||
$SNMP::use_numeric = 1;
|
||||
my $snmp = App::Netdisco::Transport::SNMP->reader_for($device)
|
||||
or return Status->defer("snapshot failed: could not SNMP connect to $device");
|
||||
|
||||
my %oidmap = getoidmap($device, $snmp);
|
||||
my %walk = walker($device, $snmp, '.1.3.6.1'); # 10205 rows
|
||||
# my %walk = walker($device, $snmp, '.1.3.6.1.2.1.2.2.1.6'); # 22 rows, i_mac/ifPhysAddress
|
||||
|
||||
my %munge = %{ $snmp->munge() };
|
||||
my %munge_set = ();
|
||||
|
||||
# take the snmpwalk of the device which is numeric (no MIB translateObj),
|
||||
# resolve to MIB identifiers using netdisco-mibs, then store in SNMP::Info
|
||||
# instance cache
|
||||
|
||||
my (%tables, %leaves, @realoids) = ((), (), ());
|
||||
OID: foreach my $orig_oid (keys %walk) {
|
||||
my $oid = $orig_oid;
|
||||
my $idx = '';
|
||||
|
||||
while (length($oid) and !exists $oidmap{$oid}) {
|
||||
$oid =~ s/\.(\d+)$//;
|
||||
$idx = ($idx ? "${1}.${idx}" : $1);
|
||||
}
|
||||
|
||||
if (exists $oidmap{$oid}) {
|
||||
$idx =~ s/^\.//;
|
||||
my $leaf = $oidmap{$oid};
|
||||
|
||||
if ($idx eq 0) {
|
||||
push @realoids, $oid;
|
||||
$leaves{ $leaf } = $walk{$orig_oid};
|
||||
$munge_set{$leaf} = subname($munge{$leaf}) if exists $munge{$leaf};
|
||||
}
|
||||
else {
|
||||
push @realoids, $oid if !exists $tables{ $leaf };
|
||||
$tables{ $leaf }->{$idx} = $walk{$orig_oid};
|
||||
$munge_set{$leaf} = subname($munge{$leaf}) if exists $munge{$leaf};
|
||||
}
|
||||
|
||||
# debug "snapshot $device - cached $oidmap{$oid}($idx)";
|
||||
next OID;
|
||||
}
|
||||
|
||||
debug "snapshot $device - missing OID $orig_oid in netdisco-mibs";
|
||||
}
|
||||
|
||||
$snmp->_cache($_, $leaves{$_}) for keys %leaves;
|
||||
$snmp->_cache($_, $tables{$_}) for keys %tables;
|
||||
|
||||
# we want to add in the GLOBALS and FUNCS aliases which users
|
||||
# have created in the SNMP::Info device class, with binary copy
|
||||
# of data so that it can be frozen
|
||||
|
||||
my %cache = %{ $snmp->cache() };
|
||||
my %funcs = %{ $snmp->funcs() };
|
||||
my %globals = %{ $snmp->globals() };
|
||||
|
||||
while (my ($alias, $leaf) = each %globals) {
|
||||
if (exists $cache{"_$leaf"} and !exists $cache{"_$alias"}) {
|
||||
$snmp->_cache($alias, $cache{"_$leaf"});
|
||||
}
|
||||
$munge_set{$leaf} = subname($munge{$alias}) if exists $munge{$alias};
|
||||
}
|
||||
|
||||
while (my ($alias, $leaf) = each %funcs) {
|
||||
if (exists $cache{store}->{$leaf} and !exists $cache{store}->{$alias}) {
|
||||
$snmp->_cache($alias, dclone $cache{store}->{$leaf});
|
||||
}
|
||||
$munge_set{$leaf} = subname($munge{$alias}) if exists $munge{$alias};
|
||||
}
|
||||
|
||||
# now for any other SNMP::Info method in GLOBALS or FUNCS which Netdisco
|
||||
# might call, but will not have data, we fake a cache entry to avoid
|
||||
# throwing errors
|
||||
|
||||
# refresh the cache
|
||||
%cache = %{ $snmp->cache() };
|
||||
|
||||
while (my $method = <DATA>) {
|
||||
$method =~ s/\s//g;
|
||||
next unless length $method and !exists $cache{"_$method"};
|
||||
|
||||
$snmp->_cache($method, {}) if exists $funcs{$method};
|
||||
$snmp->_cache($method, '') if exists $globals{$method};
|
||||
}
|
||||
|
||||
# finally, freeze the cache, then base64 encode, store in the DB, and
|
||||
# optionally save file.
|
||||
|
||||
# refresh the cache again
|
||||
%cache = %{ $snmp->cache() };
|
||||
|
||||
debug "snapshot $device - cacheing snapshot bundle";
|
||||
my $frozen = encode_base64( nfreeze( \%cache ) );
|
||||
$device->update_or_create_related('snapshot', {cache => $frozen});
|
||||
|
||||
if ($save_db) {
|
||||
debug "snapshot $device - cacheing snapshot for browsing";
|
||||
my @browser = map {{
|
||||
oid => $_,
|
||||
oid_parts => [ grep {length} (split m/\./, $_) ],
|
||||
leaf => $oidmap{$_},
|
||||
munge => $munge_set{ $oidmap{$_} },
|
||||
value => do { my $m = $oidmap{$_}; encode_base64( nfreeze( [$snmp->$m] ) ); },
|
||||
}} sort {sortable_oid($a) cmp sortable_oid($b)} @realoids;
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $gone = $device->oids->delete;
|
||||
debug sprintf ' [%s] snapshot - removed %d oids',
|
||||
$device->ip, $gone;
|
||||
$device->oids->populate(\@browser);
|
||||
debug sprintf ' [%s] snapshot - added %d new oids',
|
||||
$device->ip, scalar @browser;
|
||||
});
|
||||
}
|
||||
|
||||
if ($save_file) {
|
||||
my $target_dir = catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'logs', 'snapshots');
|
||||
make_path($target_dir);
|
||||
my $target_file = catfile($target_dir, $device->ip);
|
||||
debug "snapshot $device - saving snapshot to $target_file";
|
||||
write_text($target_file, $frozen);
|
||||
}
|
||||
|
||||
return Status->done(
|
||||
sprintf "Snapshot data captured from %s", $device->ip);
|
||||
});
|
||||
|
||||
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
# read in netdisco-mibs translation report and make an OID -> leafname map
|
||||
sub getoidmap {
|
||||
my ($device, $snmp) = @_;
|
||||
debug "snapshot $device - loading netdisco-mibs object cache";
|
||||
|
||||
my $home = (setting('mibhome') || catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'netdisco-mibs'));
|
||||
my @report = read_lines(catfile($home, qw(EXTRAS reports all_oids)), 'latin-1');
|
||||
|
||||
my %oidmap = ();
|
||||
foreach my $line (@report) {
|
||||
my ($oid, $qual_leaf, $rest) = split m/,/, $line;
|
||||
next unless defined $oid and defined $qual_leaf;
|
||||
my ($mib, $leaf) = split m/::/, $qual_leaf;
|
||||
$oidmap{$oid} = $leaf;
|
||||
}
|
||||
|
||||
debug sprintf "snapshot $device - loaded %d objects from netdisco-mibs",
|
||||
scalar keys %oidmap;
|
||||
return %oidmap;
|
||||
}
|
||||
|
||||
# taken from SNMP::Info and adjusted to work on walks outside a single table
|
||||
sub walker {
|
||||
my ($device, $snmp, $base) = @_;
|
||||
$base ||= '.1';
|
||||
|
||||
my $sess = $snmp->session();
|
||||
return unless defined $sess;
|
||||
|
||||
my $REPEATERS = 20;
|
||||
my $ver = $snmp->snmp_ver();
|
||||
|
||||
# We want the qualified leaf name so that we can
|
||||
# specify the Module (MIB) in the case of private leaf naming
|
||||
# conflicts. Example: ALTEON-TIGON-SWITCH-MIB::agSoftwareVersion
|
||||
# and ALTEON-CHEETAH-SWITCH-MIB::agSoftwareVersion
|
||||
# Third argument to translateObj specifies the Module prefix
|
||||
|
||||
my $qual_leaf = SNMP::translateObj($base,0,1) || '';
|
||||
|
||||
# We still want just the leaf since a SNMP get in the case of a
|
||||
# partial fetch may strip the Module portion upon return. We need
|
||||
# the match to make sure we didn't leave the table during getnext
|
||||
# requests
|
||||
|
||||
my ($leaf) = $qual_leaf =~ /::(.+)$/;
|
||||
|
||||
# If we weren't able to translate, we'll only have an OID
|
||||
$leaf = $base unless defined $leaf;
|
||||
|
||||
# debug "snapshot $device - $base translated as $qual_leaf";
|
||||
my $var = SNMP::Varbind->new( [$qual_leaf] );
|
||||
|
||||
# So devices speaking SNMP v.1 are not supposed to give out
|
||||
# data from SNMP2, but most do. Net-SNMP, being very precise
|
||||
# will tell you that the SNMP OID doesn't exist for the device.
|
||||
# They have a flag RetryNoSuch that is used for get() operations,
|
||||
# but not for getnext(). We set this flag normally, and if we're
|
||||
# using V1, let's try and fetch the data even if we get one of those.
|
||||
|
||||
my %localstore = ();
|
||||
my $errornum = 0;
|
||||
my %seen = ();
|
||||
|
||||
my $vars = [];
|
||||
my $bulkwalk_no
|
||||
= $snmp->can('bulkwalk_no') ? $snmp->bulkwalk_no() : 0;
|
||||
my $bulkwalk_on = defined $snmp->{BulkWalk} ? $snmp->{BulkWalk} : 1;
|
||||
my $can_bulkwalk = $bulkwalk_on && !$bulkwalk_no;
|
||||
my $repeaters = $snmp->{BulkRepeaters} || $REPEATERS;
|
||||
my $bulkwalk = $can_bulkwalk && $ver != 1;
|
||||
my $loopdetect
|
||||
= defined $snmp->{LoopDetect} ? $snmp->{LoopDetect} : 1;
|
||||
|
||||
debug "snapshot $device - starting walk from $base";
|
||||
|
||||
# Use BULKWALK if we can because its faster
|
||||
if ( $bulkwalk && @$vars == 0 ) {
|
||||
($vars) = $sess->bulkwalk( 0, $repeaters, $var );
|
||||
if ( $sess->{ErrorNum} ) {
|
||||
error "snapshot $device BULKWALK " . $sess->{ErrorStr};
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while ( !$errornum ) {
|
||||
if ($bulkwalk) {
|
||||
$var = shift @$vars or last;
|
||||
}
|
||||
else {
|
||||
# GETNEXT instead of BULKWALK
|
||||
# debug "snapshot $device GETNEXT $var";
|
||||
$sess->getnext($var);
|
||||
$errornum = $sess->{ErrorNum};
|
||||
}
|
||||
|
||||
my $iid = $var->[1];
|
||||
my $val = $var->[2];
|
||||
my $oid = $var->[0] . (defined $iid ? ".${iid}" : '');
|
||||
|
||||
# debug "snapshot $device reading $oid";
|
||||
# p $var;
|
||||
|
||||
unless ( defined $iid ) {
|
||||
error "snapshot $device not here";
|
||||
next;
|
||||
}
|
||||
|
||||
# Check if last element, V2 devices may report ENDOFMIBVIEW even if
|
||||
# instance or object doesn't exist.
|
||||
if ( $val eq 'ENDOFMIBVIEW' ) {
|
||||
debug "snapshot $device : ENDOFMIBVIEW";
|
||||
last;
|
||||
}
|
||||
|
||||
# Similarly for SNMPv1 - noSuchName return results in both $iid
|
||||
# and $val being empty strings.
|
||||
if ( $val eq '' and $iid eq '' ) {
|
||||
debug "snapshot $device : v1 noSuchName (1)";
|
||||
last;
|
||||
}
|
||||
|
||||
# Another check for SNMPv1 - noSuchName return may results in an $oid
|
||||
# we've already seen and $val an empty string. If we don't catch
|
||||
# this here we erroneously report a loop below.
|
||||
if ( defined $seen{$oid} and $seen{$oid} and $val eq '' ) {
|
||||
debug "snapshot $device : v1 noSuchName (2)";
|
||||
last;
|
||||
}
|
||||
|
||||
if ($loopdetect) {
|
||||
# Check to see if we've already seen this IID (looping)
|
||||
if ( defined $seen{$oid} and $seen{$oid} ) {
|
||||
error "Looping on: oid:$oid. ";
|
||||
last;
|
||||
}
|
||||
else {
|
||||
$seen{$oid}++;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $val eq 'NOSUCHOBJECT' ) {
|
||||
error "snapshot $device : NOSUCHOBJECT";
|
||||
next;
|
||||
}
|
||||
if ( $val eq 'NOSUCHINSTANCE' ) {
|
||||
error "snapshot $device : NOSUCHINSTANCE";
|
||||
next;
|
||||
}
|
||||
|
||||
# debug "snapshot $device - retreived $oid : $val";
|
||||
$localstore{$oid} = $val;
|
||||
}
|
||||
|
||||
debug sprintf "snapshot $device - walked %d rows from $base",
|
||||
scalar keys %localstore;
|
||||
return %localstore;
|
||||
}
|
||||
|
||||
true;
|
||||
|
||||
__DATA__
|
||||
agg_ports
|
||||
at_paddr
|
||||
bgp_peer_addr
|
||||
bp_index
|
||||
c_cap
|
||||
c_id
|
||||
c_if
|
||||
c_ip
|
||||
c_platform
|
||||
c_port
|
||||
cd11_mac
|
||||
cd11_port
|
||||
cd11_rateset
|
||||
cd11_rxbyte
|
||||
cd11_rxpkt
|
||||
cd11_sigqual
|
||||
cd11_sigstrength
|
||||
cd11_ssid
|
||||
cd11_txbyte
|
||||
cd11_txpkt
|
||||
cd11_txrate
|
||||
cd11_uptime
|
||||
class
|
||||
contact
|
||||
docs_if_cmts_cm_status_inet_address
|
||||
dot11_cur_tx_pwr_mw
|
||||
e_class
|
||||
e_descr
|
||||
e_fru
|
||||
e_fwver
|
||||
e_hwver
|
||||
e_index
|
||||
e_model
|
||||
e_name
|
||||
e_parent
|
||||
e_pos
|
||||
e_serial
|
||||
e_swver
|
||||
e_type
|
||||
eigrp_peers
|
||||
fw_mac
|
||||
fw_port
|
||||
has_topo
|
||||
i_80211channel
|
||||
i_alias
|
||||
i_description
|
||||
i_duplex
|
||||
i_duplex_admin
|
||||
i_err_disable_cause
|
||||
i_faststart_enabled
|
||||
i_ignore
|
||||
i_lastchange
|
||||
i_mac
|
||||
i_mtu
|
||||
i_name
|
||||
i_speed
|
||||
i_speed_admin
|
||||
i_speed_raw
|
||||
i_ssidbcast
|
||||
i_ssidlist
|
||||
i_ssidmac
|
||||
i_stp_state
|
||||
i_type
|
||||
i_up
|
||||
i_up_admin
|
||||
i_vlan
|
||||
i_vlan_membership
|
||||
i_vlan_membership_untagged
|
||||
i_vlan_type
|
||||
interfaces
|
||||
ip_index
|
||||
ip_netmask
|
||||
ipv6_addr
|
||||
ipv6_addr_prefixlength
|
||||
ipv6_index
|
||||
ipv6_n2p_mac
|
||||
ipv6_type
|
||||
isis_peers
|
||||
lldp_ipv6
|
||||
lldp_media_cap
|
||||
lldp_rem_model
|
||||
lldp_rem_serial
|
||||
lldp_rem_sw_rev
|
||||
lldp_rem_vendor
|
||||
location
|
||||
model
|
||||
name
|
||||
ospf_peer_id
|
||||
ospf_peers
|
||||
peth_port_admin
|
||||
peth_port_class
|
||||
peth_port_ifindex
|
||||
peth_port_power
|
||||
peth_port_status
|
||||
peth_power_status
|
||||
peth_power_watts
|
||||
ports
|
||||
qb_fw_vlan
|
||||
serial
|
||||
serial1
|
||||
snmpEngineID
|
||||
snmpEngineTime
|
||||
snmp_comm
|
||||
snmp_ver
|
||||
v_index
|
||||
v_name
|
||||
vrf_name
|
||||
vtp_d_name
|
||||
vtp_version
|
||||
Reference in New Issue
Block a user