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:
Oliver Gorwits
2021-11-06 07:47:29 +00:00
committed by GitHub
parent 74210dd78f
commit dc1f76c1aa
59 changed files with 10099 additions and 44 deletions

3
.gitattributes vendored
View File

@@ -1,5 +1,8 @@
share/public/javascripts/*.map binary share/public/javascripts/*.map binary
share/public/javascripts/*.min.js binary share/public/javascripts/*.min.js binary
share/public/javascripts/jquery-history.js binary share/public/javascripts/jquery-history.js binary
share/public/css/smoothness/jquery-ui.custom.min.css binary
share/public/javascripts/jstree/jstree.min.js binary
share/public/css/toastr.css binary
share/public/css/*.min.css binary share/public/css/*.min.css binary
share/public/swagger-ui/* binary share/public/swagger-ui/* binary

View File

@@ -43,6 +43,8 @@ Module::Build->new(
'Dancer::Plugin::Swagger' => '0', 'Dancer::Plugin::Swagger' => '0',
'Dancer::Session::Cookie' => '0.27', 'Dancer::Session::Cookie' => '0.27',
'Expect' => '0', 'Expect' => '0',
'File::Path' => '0',
'File::Spec::Functions' => '0',
'File::ShareDir' => '1.03', 'File::ShareDir' => '1.03',
'File::Slurper' => '0.009', 'File::Slurper' => '0.009',
'Guard' => '1.022', 'Guard' => '1.022',
@@ -51,6 +53,7 @@ Module::Build->new(
'IO::Socket::INET6' => '2.72', 'IO::Socket::INET6' => '2.72',
'IO::Socket::SSL' => '2.048', 'IO::Socket::SSL' => '2.048',
'JSON' => '2.90', 'JSON' => '2.90',
'JSON::PP' => '0',
'JSON::XS' => '3.01', 'JSON::XS' => '3.01',
'List::Util' => '1.49', 'List::Util' => '1.49',
'List::MoreUtils' => '0.428', 'List::MoreUtils' => '0.428',
@@ -86,6 +89,7 @@ Module::Build->new(
'SNMP::Info' => '3.81', 'SNMP::Info' => '3.81',
'SQL::Abstract' => '1.85', 'SQL::Abstract' => '1.85',
'SQL::Translator' => '0.11024', 'SQL::Translator' => '0.11024',
'Sub::Util' => '1.40',
'Template' => '2.24', 'Template' => '2.24',
'Template::AutoFilter' => '0', 'Template::AutoFilter' => '0',
'Template::Plugin::CSV' => '0.04', 'Template::Plugin::CSV' => '0.04',

View File

@@ -26,6 +26,7 @@ lib/App/Netdisco/DB/Result.pm
lib/App/Netdisco/DB/Result/Admin.pm lib/App/Netdisco/DB/Result/Admin.pm
lib/App/Netdisco/DB/Result/Community.pm lib/App/Netdisco/DB/Result/Community.pm
lib/App/Netdisco/DB/Result/Device.pm lib/App/Netdisco/DB/Result/Device.pm
lib/App/Netdisco/DB/Result/DeviceBrowser.pm
lib/App/Netdisco/DB/Result/DeviceIp.pm lib/App/Netdisco/DB/Result/DeviceIp.pm
lib/App/Netdisco/DB/Result/DeviceModule.pm lib/App/Netdisco/DB/Result/DeviceModule.pm
lib/App/Netdisco/DB/Result/DevicePort.pm lib/App/Netdisco/DB/Result/DevicePort.pm
@@ -37,6 +38,7 @@ lib/App/Netdisco/DB/Result/DevicePortVlan.pm
lib/App/Netdisco/DB/Result/DevicePortWireless.pm lib/App/Netdisco/DB/Result/DevicePortWireless.pm
lib/App/Netdisco/DB/Result/DevicePower.pm lib/App/Netdisco/DB/Result/DevicePower.pm
lib/App/Netdisco/DB/Result/DeviceSkip.pm lib/App/Netdisco/DB/Result/DeviceSkip.pm
lib/App/Netdisco/DB/Result/DeviceSnapshot.pm
lib/App/Netdisco/DB/Result/DeviceVlan.pm lib/App/Netdisco/DB/Result/DeviceVlan.pm
lib/App/Netdisco/DB/Result/Log.pm lib/App/Netdisco/DB/Result/Log.pm
lib/App/Netdisco/DB/Result/NetmapPositions.pm lib/App/Netdisco/DB/Result/NetmapPositions.pm
@@ -48,6 +50,7 @@ lib/App/Netdisco/DB/Result/NodeWireless.pm
lib/App/Netdisco/DB/Result/Oui.pm lib/App/Netdisco/DB/Result/Oui.pm
lib/App/Netdisco/DB/Result/Process.pm lib/App/Netdisco/DB/Result/Process.pm
lib/App/Netdisco/DB/Result/Session.pm lib/App/Netdisco/DB/Result/Session.pm
lib/App/Netdisco/DB/Result/SNMPObject.pm
lib/App/Netdisco/DB/Result/Statistics.pm lib/App/Netdisco/DB/Result/Statistics.pm
lib/App/Netdisco/DB/Result/Subnet.pm lib/App/Netdisco/DB/Result/Subnet.pm
lib/App/Netdisco/DB/Result/Topology.pm lib/App/Netdisco/DB/Result/Topology.pm
@@ -62,6 +65,7 @@ lib/App/Netdisco/DB/Result/Virtual/DeviceLinks.pm
lib/App/Netdisco/DB/Result/Virtual/DevicePoeStatus.pm lib/App/Netdisco/DB/Result/Virtual/DevicePoeStatus.pm
lib/App/Netdisco/DB/Result/Virtual/DevicePortSpeed.pm lib/App/Netdisco/DB/Result/Virtual/DevicePortSpeed.pm
lib/App/Netdisco/DB/Result/Virtual/DuplexMismatch.pm lib/App/Netdisco/DB/Result/Virtual/DuplexMismatch.pm
lib/App/Netdisco/DB/Result/Virtual/FilteredSNMPObject.pm
lib/App/Netdisco/DB/Result/Virtual/GenericReport.pm lib/App/Netdisco/DB/Result/Virtual/GenericReport.pm
lib/App/Netdisco/DB/Result/Virtual/LastNode.pm lib/App/Netdisco/DB/Result/Virtual/LastNode.pm
lib/App/Netdisco/DB/Result/Virtual/NodeIp4.pm lib/App/Netdisco/DB/Result/Virtual/NodeIp4.pm
@@ -69,6 +73,7 @@ lib/App/Netdisco/DB/Result/Virtual/NodeIp6.pm
lib/App/Netdisco/DB/Result/Virtual/NodeMonitor.pm lib/App/Netdisco/DB/Result/Virtual/NodeMonitor.pm
lib/App/Netdisco/DB/Result/Virtual/NodesDiscovered.pm lib/App/Netdisco/DB/Result/Virtual/NodesDiscovered.pm
lib/App/Netdisco/DB/Result/Virtual/NodeWithAge.pm lib/App/Netdisco/DB/Result/Virtual/NodeWithAge.pm
lib/App/Netdisco/DB/Result/Virtual/OidChildren.pm
lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm
lib/App/Netdisco/DB/Result/Virtual/PollerPerformance.pm lib/App/Netdisco/DB/Result/Virtual/PollerPerformance.pm
lib/App/Netdisco/DB/Result/Virtual/PortMacs.pm lib/App/Netdisco/DB/Result/Virtual/PortMacs.pm
@@ -83,6 +88,7 @@ lib/App/Netdisco/DB/Result/Virtual/UserRole.pm
lib/App/Netdisco/DB/ResultSet.pm lib/App/Netdisco/DB/ResultSet.pm
lib/App/Netdisco/DB/ResultSet/Admin.pm lib/App/Netdisco/DB/ResultSet/Admin.pm
lib/App/Netdisco/DB/ResultSet/Device.pm lib/App/Netdisco/DB/ResultSet/Device.pm
lib/App/Netdisco/DB/ResultSet/DeviceBrowser.pm
lib/App/Netdisco/DB/ResultSet/DeviceModule.pm lib/App/Netdisco/DB/ResultSet/DeviceModule.pm
lib/App/Netdisco/DB/ResultSet/DevicePort.pm lib/App/Netdisco/DB/ResultSet/DevicePort.pm
lib/App/Netdisco/DB/ResultSet/DevicePortLog.pm lib/App/Netdisco/DB/ResultSet/DevicePortLog.pm
@@ -159,6 +165,7 @@ lib/App/Netdisco/Web/Plugin/Device/Details.pm
lib/App/Netdisco/Web/Plugin/Device/Modules.pm lib/App/Netdisco/Web/Plugin/Device/Modules.pm
lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm lib/App/Netdisco/Web/Plugin/Device/Neighbors.pm
lib/App/Netdisco/Web/Plugin/Device/Ports.pm lib/App/Netdisco/Web/Plugin/Device/Ports.pm
lib/App/Netdisco/Web/Plugin/Device/SNMP.pm
lib/App/Netdisco/Web/Plugin/Device/Vlans.pm lib/App/Netdisco/Web/Plugin/Device/Vlans.pm
lib/App/Netdisco/Web/Plugin/Inventory.pm lib/App/Netdisco/Web/Plugin/Inventory.pm
lib/App/Netdisco/Web/Plugin/Report/ApChannelDist.pm lib/App/Netdisco/Web/Plugin/Report/ApChannelDist.pm
@@ -228,6 +235,7 @@ lib/App/Netdisco/Worker/Plugin/Graph.pm
lib/App/Netdisco/Worker/Plugin/Hook.pm lib/App/Netdisco/Worker/Plugin/Hook.pm
lib/App/Netdisco/Worker/Plugin/Hook/Exec.pm lib/App/Netdisco/Worker/Plugin/Hook/Exec.pm
lib/App/Netdisco/Worker/Plugin/Hook/HTTP.pm lib/App/Netdisco/Worker/Plugin/Hook/HTTP.pm
lib/App/Netdisco/Worker/Plugin/LoadMIBs.pm
lib/App/Netdisco/Worker/Plugin/Location.pm lib/App/Netdisco/Worker/Plugin/Location.pm
lib/App/Netdisco/Worker/Plugin/Macsuck.pm lib/App/Netdisco/Worker/Plugin/Macsuck.pm
lib/App/Netdisco/Worker/Plugin/Macsuck/Hooks.pm lib/App/Netdisco/Worker/Plugin/Macsuck/Hooks.pm
@@ -245,6 +253,7 @@ lib/App/Netdisco/Worker/Plugin/Power.pm
lib/App/Netdisco/Worker/Plugin/Psql.pm lib/App/Netdisco/Worker/Plugin/Psql.pm
lib/App/Netdisco/Worker/Plugin/Renumber.pm lib/App/Netdisco/Worker/Plugin/Renumber.pm
lib/App/Netdisco/Worker/Plugin/Show.pm lib/App/Netdisco/Worker/Plugin/Show.pm
lib/App/Netdisco/Worker/Plugin/Snapshot.pm
lib/App/Netdisco/Worker/Plugin/Stats.pm lib/App/Netdisco/Worker/Plugin/Stats.pm
lib/App/Netdisco/Worker/Plugin/Vlan.pm lib/App/Netdisco/Worker/Plugin/Vlan.pm
lib/App/Netdisco/Worker/Plugin/Vlan/Core.pm lib/App/Netdisco/Worker/Plugin/Vlan/Core.pm
@@ -343,6 +352,24 @@ share/public/javascripts/jquery.cookie.js
share/public/javascripts/jquery.dataTables.min.js share/public/javascripts/jquery.dataTables.min.js
share/public/javascripts/jquery.floatThead.js share/public/javascripts/jquery.floatThead.js
share/public/javascripts/jquery.qtip.min.js share/public/javascripts/jquery.qtip.min.js
share/public/javascripts/jstree/jstree.min.js
share/public/javascripts/jstree/themes/proton/30px.png
share/public/javascripts/jstree/themes/proton/32px.png
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-bold-webfont.eot
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-bold-webfont.svg
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-bold-webfont.ttf
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-bold-webfont.woff
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-extralight-webfont.eot
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-extralight-webfont.svg
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-extralight-webfont.ttf
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-extralight-webfont.woff
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-regular-webfont.eot
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-regular-webfont.svg
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-regular-webfont.ttf
share/public/javascripts/jstree/themes/proton/fonts/titillium/titilliumweb-regular-webfont.woff
share/public/javascripts/jstree/themes/proton/style.css
share/public/javascripts/jstree/themes/proton/style.min.css
share/public/javascripts/jstree/themes/proton/throbber.gif
share/public/javascripts/moment.min.js share/public/javascripts/moment.min.js
share/public/javascripts/natural.js share/public/javascripts/natural.js
share/public/javascripts/netdisco.js share/public/javascripts/netdisco.js
@@ -429,6 +456,10 @@ share/schema_versions/App-Netdisco-DB-62-63-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-63-64-PostgreSQL.sql share/schema_versions/App-Netdisco-DB-63-64-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-64-65-PostgreSQL.sql share/schema_versions/App-Netdisco-DB-64-65-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-65-66-PostgreSQL.sql share/schema_versions/App-Netdisco-DB-65-66-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-66-67-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-67-68-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-68-69-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-69-70-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-7-8-PostgreSQL.sql share/schema_versions/App-Netdisco-DB-7-8-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-8-9-PostgreSQL.sql share/schema_versions/App-Netdisco-DB-8-9-PostgreSQL.sql
share/schema_versions/App-Netdisco-DB-9-10-PostgreSQL.sql share/schema_versions/App-Netdisco-DB-9-10-PostgreSQL.sql
@@ -456,6 +487,8 @@ share/views/ajax/device/modules.tt
share/views/ajax/device/netmap.tt share/views/ajax/device/netmap.tt
share/views/ajax/device/ports.tt share/views/ajax/device/ports.tt
share/views/ajax/device/ports_csv.tt share/views/ajax/device/ports_csv.tt
share/views/ajax/device/snmp.tt
share/views/ajax/device/snmpnode.tt
share/views/ajax/device/vlans.tt share/views/ajax/device/vlans.tt
share/views/ajax/device/vlans_csv.tt share/views/ajax/device/vlans_csv.tt
share/views/ajax/report/apchanneldist.tt share/views/ajax/report/apchanneldist.tt

View File

@@ -422,6 +422,24 @@ Pass a device in C<-d> to display them:
~/bin/netdisco-do dumpconfig -d 192.0.2.1 -e device_auth ~/bin/netdisco-do dumpconfig -d 192.0.2.1 -e device_auth
=head2 snapshot
Performs an snmp walk from L<.1.3.6.1> on the device and builds a data
structure which SNMP::Info can use to mimic the device. The structure is
saved into the L<device_snapshot> database table.
Pass a value to the L<-p> parameter (mnemonic: persist) and the base64 encoded
data will also be saved to L<NETDISCO_HOME/logs/snapshots/IP> (where L<IP> is
the canonical IP of the device passed). Netdisco will load this for any pseudo
device with the same canonical IP.
~/bin/netdisco-do snapshot -d 192.0.2.1 -p yes
Pass a value to the L<-e> parameter (mnemonic: explore> and the data is also
saved into the Netdisco database for web browsing (under Device->SNMP tab).
~/bin/netdisco-do snapshot -d 192.0.2.1 -e yes
=head1 DEBUG OPTIONS =head1 DEBUG OPTIONS
The flag "C<-R>" will cause any changes to the database to be rolled back The flag "C<-R>" will cause any changes to the database to be rolled back

View File

@@ -11,7 +11,7 @@ __PACKAGE__->load_namespaces(
); );
our # try to hide from kwalitee 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 Path::Class;
use File::ShareDir 'dist_dir'; use File::ShareDir 'dist_dir';

View File

@@ -81,6 +81,8 @@ __PACKAGE__->add_columns(
{ data_type => "timestamp", is_nullable => 1 }, { data_type => "timestamp", is_nullable => 1 },
"last_arpnip", "last_arpnip",
{ data_type => "timestamp", is_nullable => 1 }, { data_type => "timestamp", is_nullable => 1 },
"is_pseudo",
{ data_type => "boolean", is_nullable => 0, default_value => \"false" },
); );
__PACKAGE__->set_primary_key("ip"); __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' ); __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 =head2 port_vlans
Returns the set of VLANs known to be configured on Ports on this Device, 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( __PACKAGE__->might_have(
community => 'App::Netdisco::DB::Result::Community', 'ip'); 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 =head2 throughput
Returns a sum of speeds on all ports on the device. Returns a sum of speeds on all ports on the device.
@@ -267,17 +286,6 @@ __PACKAGE__->has_one(
=head1 ADDITIONAL METHODS =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 ) =head2 has_layer( $number )
Returns true if the device provided sysServices and supports the given layer. 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 # Community is not included as SNMP::test_connection will take care of it
foreach my $set (qw/ foreach my $set (qw/
DeviceBrowser
DeviceIp DeviceIp
DeviceModule DeviceModule
DevicePower
DeviceSnapshot
DeviceVlan
DevicePort DevicePort
DevicePortLog DevicePortLog
DevicePortPower DevicePortPower
@@ -323,8 +335,6 @@ sub renumber {
DevicePortSsid DevicePortSsid
DevicePortVlan DevicePortVlan
DevicePortWireless DevicePortWireless
DevicePower
DeviceVlan
/) { /) {
$schema->resultset($set) $schema->resultset($set)
->search({ip => $old_ip}) ->search({ip => $old_ip})
@@ -353,6 +363,11 @@ sub renumber {
->search({dev2 => $old_ip}) ->search({dev2 => $old_ip})
->update({dev2 => $new_ip}); ->update({dev2 => $new_ip});
$schema->resultset('Admin')->search({
device => $old_ip,
status => { '-not_like' => 'queued-%' },
})->delete;
$device->update({ $device->update({
ip => $new_ip, ip => $new_ip,
dns => hostname_from_ip($new_ip), dns => hostname_from_ip($new_ip),

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

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

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

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

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

View File

@@ -704,11 +704,13 @@ sub delete {
$ip ||= 'netdisco'; $ip ||= 'netdisco';
foreach my $set (qw/ foreach my $set (qw/
DeviceIp
DeviceVlan
DevicePower
DeviceModule
Community Community
DeviceBrowser
DeviceIp
DeviceModule
DevicePower
DeviceSnapshot
DeviceVlan
/) { /) {
my $gone = $schema->resultset($set)->search( my $gone = $schema->resultset($set)->search(
{ ip => { '-in' => $devices->as_query } }, { ip => { '-in' => $devices->as_query } },
@@ -746,3 +748,35 @@ sub delete {
} }
1; 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

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

View File

@@ -254,9 +254,9 @@ sub delete {
foreach my $set (qw/ foreach my $set (qw/
DevicePortPower DevicePortPower
DevicePortProperties DevicePortProperties
DevicePortSsid
DevicePortVlan DevicePortVlan
DevicePortWireless DevicePortWireless
DevicePortSsid
/) { /) {
my $gone = $schema->resultset($set)->search( my $gone = $schema->resultset($set)->search(
{ ip => { '-in' => $ports->as_query }}, { ip => { '-in' => $ports->as_query }},

View File

@@ -10,7 +10,12 @@ use App::Netdisco::Util::Permission ':all';
use SNMP::Info; use SNMP::Info;
use Try::Tiny; use Try::Tiny;
use Module::Load (); use Module::Load ();
use Storable 'thaw';
use File::Slurper 'read_text';
use MIME::Base64 'decode_base64';
use Path::Class 'dir'; use Path::Class 'dir';
use File::Path 'make_path';
use File::Spec::Functions qw(catdir catfile);
use NetAddr::IP::Lite ':lower'; use NetAddr::IP::Lite ':lower';
use List::Util qw/pairkeys pairfirst/; use List::Util qw/pairkeys pairfirst/;
@@ -58,7 +63,9 @@ Returns C<undef> if the connection fails.
sub reader_for { sub reader_for {
my ($class, $ip, $useclass) = @_; my ($class, $ip, $useclass) = @_;
my $device = get_device($ip) or return undef; 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; my $readers = $class->instance->readers or return undef;
return $readers->{$device->ip} if exists $readers->{$device->ip}; return $readers->{$device->ip} if exists $readers->{$device->ip};
@@ -107,6 +114,7 @@ Returns C<undef> if the connection fails.
sub writer_for { sub writer_for {
my ($class, $ip, $useclass) = @_; my ($class, $ip, $useclass) = @_;
my $device = get_device($ip) or return undef; my $device = get_device($ip) or return undef;
return undef if $device->in_storage and $device->is_pseudo; return undef if $device->in_storage and $device->is_pseudo;
my $writers = $class->instance->writers or return undef; my $writers = $class->instance->writers or return undef;
@@ -155,6 +163,13 @@ sub _snmp_connect_generic {
$snmp_args{BulkWalk} = 0; $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) # get the community string(s)
my @communities = get_communities($device, $mode); my @communities = get_communities($device, $mode);
@@ -242,7 +257,7 @@ sub _try_read {
return undef unless ( return undef unless (
(not defined $info->error) (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->layers or $info->description)
and $info->class and $info->class
); );

View File

@@ -4,6 +4,9 @@ use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema'; use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Permission qw/check_acl_no check_acl_only/; 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'; use base 'Exporter';
our @EXPORT = (); our @EXPORT = ();
our @EXPORT_OK = qw/ 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 Also respects C<discover_phones> and C<discover_waps> if either are set to
false. 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. Returns false if the host is not permitted to discover the target device.
@@ -170,8 +173,9 @@ sub is_discoverable {
$remote_type ||= ''; $remote_type ||= '';
$remote_cap ||= []; $remote_cap ||= [];
return _bail_msg("is_discoverable: $device is pseudo-device") my $pseudo_cache = catfile( catdir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'logs', 'snapshots'), $device->ip );
if $device->is_pseudo; 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") return _bail_msg("is_discoverable: $device matches wap_platforms but discover_waps is not enabled")
if ((not setting('discover_waps')) and if ((not setting('discover_waps')) and

View File

@@ -1,11 +1,21 @@
package App::Netdisco::Util::SNMP; 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 App::Netdisco::Util::DeviceAuth 'get_external_credentials';
use MIME::Base64 'decode_base64';
use Storable 'thaw';
use JSON::PP;
use base 'Exporter'; use base 'Exporter';
our @EXPORT = (); 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); our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME =head1 NAME
@@ -21,6 +31,24 @@ subroutines.
=head1 EXPORT_OK =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 ) =head2 get_communities( $device, $mode )
Takes the current C<device_auth> setting and pushes onto the front of the list 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; 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; true;

View File

@@ -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 { get '/admin/*' => require_role admin => sub {
my ($tag) = splat; my ($tag) = splat;

View File

@@ -81,6 +81,7 @@ get '/device' => require_login sub {
params->{'tab'} ||= 'details'; params->{'tab'} ||= 'details';
template 'device', { template 'device', {
is_pseudo => $first->is_pseudo,
display_name => ($others ? $first->ip : ($first->dns || $first->ip)), display_name => ($others ? $first->ip : ($first->dns || $first->ip)),
lgroup_list => [ schema('netdisco')->resultset('Device')->get_distinct_col('location') ], lgroup_list => [ schema('netdisco')->resultset('Device')->get_distinct_col('location') ],
hgroup_list => setting('host_group_displaynames'), hgroup_list => setting('host_group_displaynames'),

View File

@@ -170,6 +170,7 @@ register 'register_search_tab' => sub {
register 'register_device_tab' => sub { register 'register_device_tab' => sub {
my ($self, $config) = plugin_args(@_); my ($self, $config) = plugin_args(@_);
$config->{render_if} ||= sub { true };
_register_tab('device', $config); _register_tab('device', $config);
}; };

View File

@@ -38,6 +38,7 @@ ajax '/ajax/control/admin/pseudodevice/add' => require_role admin => sub {
vendor => 'netdisco', vendor => 'netdisco',
layers => param('layers'), layers => param('layers'),
last_discover => \'now()', last_discover => \'now()',
is_pseudo => \'true',
}); });
return unless $device; 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 { ajax '/ajax/content/admin/pseudodevice' => require_role admin => sub {
my $set = schema('netdisco')->resultset('Device') my $set = schema('netdisco')->resultset('Device')
->search( ->search(
{vendor => 'netdisco'}, {-bool => 'is_pseudo'},
{order_by => { -desc => 'last_discover' }}, {order_by => { -desc => 'last_discover' }},
)->with_port_count; )->with_port_count;

View File

@@ -17,7 +17,13 @@ ajax '/ajax/content/device/details' => require_login sub {
my @results my @results
= schema('netdisco')->resultset('Device') = 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; ->hri->all;
my @power my @power

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

View File

@@ -38,14 +38,14 @@ sub gather_subnets {
my $ip_netmask = $snmp->ip_netmask; my $ip_netmask = $snmp->ip_netmask;
foreach my $entry (keys %$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; my $addr = $ip->addr;
next if $addr eq '0.0.0.0'; next if $addr eq '0.0.0.0';
next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__'); next if check_acl_no($ip, 'group:__LOCAL_ADDRESSES__');
next if setting('ignore_private_nets') and $ip->is_rfc1918; 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'; 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; my $cidr = NetAddr::IP::Lite->new($addr, $netmask)->network->cidr;

View File

@@ -61,7 +61,7 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub {
$device->set_column( last_discover => \'now()' ); $device->set_column( last_discover => \'now()' );
# protection for failed SNMP gather # protection for failed SNMP gather
if ($device->in_storage) { if ($device->in_storage and not $device->is_pseudo) {
my $ip = $device->ip; my $ip = $device->ip;
my $protect = setting('snmp_field_protection')->{'device'} || {}; my $protect = setting('snmp_field_protection')->{'device'} || {};
my %dirty = $device->get_dirty_columns; my %dirty = $device->get_dirty_columns;
@@ -199,7 +199,7 @@ register_worker({ phase => 'early', driver => 'snmp' }, sub {
my $agg_ports = $snmp->agg_ports; my $agg_ports = $snmp->agg_ports;
# clear the cached uptime and get a new one # 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) { if (!defined $dev_uptime) {
error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!', error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!',
$device->ip; $device->ip;

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

View File

@@ -28,8 +28,8 @@ register_worker({ phase => 'main', driver => 'snmp' }, sub {
} }
} }
my $i = App::Netdisco::Transport::SNMP->reader_for($device, $class); my $snmp = App::Netdisco::Transport::SNMP->reader_for($device, $class);
my $result = sub { eval { $i->$extra($port) } || undef }; my $result = sub { eval { $snmp->$extra($port) } || undef };
Data::Printer::p( $result->() ); Data::Printer::p( $result->() );
return Status->done( return Status->done(

View 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

View File

@@ -92,6 +92,7 @@ web_plugins:
- Device::Neighbors - Device::Neighbors
- Device::Addresses - Device::Addresses
- Device::Vlans - Device::Vlans
- Device::SNMP
extra_web_plugins: [] extra_web_plugins: []
sidebar_defaults: sidebar_defaults:
search_node: search_node:
@@ -392,51 +393,53 @@ job_prio:
worker_plugins: worker_plugins:
- 'Arpnip' - 'Arpnip'
- 'Arpnip::Hooks'
- 'Arpnip::Nodes' - 'Arpnip::Nodes'
- 'Arpnip::Subnets' - 'Arpnip::Subnets'
- 'Arpnip::Hooks'
- 'Arpwalk' - 'Arpwalk'
- 'Contact' - 'Contact'
- 'Delete' - 'Delete'
- 'Discover' - 'Discover'
- 'Discover::CanonicalIP' - 'Discover::CanonicalIP'
- 'Discover::Entities' - 'Discover::Entities'
- 'Discover::Hooks'
- 'Discover::Neighbors' - 'Discover::Neighbors'
- 'Discover::Neighbors::Routed'
- 'Discover::Neighbors::DOCSIS' - 'Discover::Neighbors::DOCSIS'
- 'Discover::Neighbors::Routed'
- 'Discover::PortPower' - 'Discover::PortPower'
- 'Discover::PortProperties' - 'Discover::PortProperties'
- 'Discover::Properties' - 'Discover::Properties'
- 'Discover::VLANs' - 'Discover::VLANs'
- 'Discover::Wireless' - 'Discover::Wireless'
- 'Discover::WithNodes' - 'Discover::WithNodes'
- 'Discover::Hooks'
- 'DiscoverAll' - 'DiscoverAll'
- 'DumpConfig' - 'DumpConfig'
- 'Expire' - 'Expire'
- 'ExpireNodes' - 'ExpireNodes'
- 'GetAPIKey'
- 'Graph' - 'Graph'
- 'Hook' - 'Hook'
- 'Hook::Exec' - 'Hook::Exec'
- 'Hook::HTTP' - 'Hook::HTTP'
- 'LoadMIBs'
- 'Location' - 'Location'
- 'Macsuck' - 'Macsuck'
- 'Macsuck::Hooks'
- 'Macsuck::Nodes' - 'Macsuck::Nodes'
- 'Macsuck::WirelessNodes' - 'Macsuck::WirelessNodes'
- 'Macsuck::Hooks'
- 'Macwalk' - 'Macwalk'
- 'MakeRancidConf' - 'MakeRancidConf'
- 'NodeMonitor'
- 'Nbtstat' - 'Nbtstat'
- 'Nbtstat::Core' - 'Nbtstat::Core'
- 'Nbtwalk' - 'Nbtwalk'
- 'NodeMonitor'
- 'PortControl' - 'PortControl'
- 'PortName' - 'PortName'
- 'Power' - 'Power'
- 'Psql' - 'Psql'
- 'Renumber' - 'Renumber'
- 'GetAPIKey'
- 'Show' - 'Show'
- 'Snapshot'
- 'Stats' - 'Stats'
- 'Vlan' - 'Vlan'
- 'Vlan::Core' - 'Vlan::Core'

View File

@@ -61,6 +61,17 @@ div.content > div.tab-content table.nd_floatinghead thead {
overflow-x: hidden; overflow-x: hidden;
} }
/* jstree scrollable */
.nd_scrollable {
height: 85vh;
overflow: auto;
}
#snmpPartialSearch {
margin-top: -3px;
}
/* fake looks for form submit buttons embedded in bootstrap dropdowns */ /* fake looks for form submit buttons embedded in bootstrap dropdowns */
.nd_btn-link { .nd_btn-link {
display: block; display: block;

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 132 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 137 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 147 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,9 @@
BEGIN;
ALTER TABLE device ADD COLUMN "is_pseudo" boolean DEFAULT false;
UPDATE device SET is_pseudo = false;
UPDATE device SET is_pseudo = true WHERE vendor = 'netdisco';
COMMIT;

View File

@@ -0,0 +1,9 @@
BEGIN;
CREATE TABLE device_snapshot (
"ip" "inet",
"cache" "text",
PRIMARY KEY ("ip")
);
COMMIT;

View File

@@ -0,0 +1,17 @@
BEGIN;
CREATE TABLE device_browser (
"ip" "inet" NOT NULL,
"oid" "text" NOT NULL,
"oid_parts" integer[] NOT NULL,
"leaf" "text" NOT NULL,
"munge" "text",
"value" "text",
PRIMARY KEY ("ip", "oid")
);
CREATE INDEX idx_device_browser_ip_leaf ON device_browser(ip, leaf);
CREATE INDEX idx_device_browser_oid__pattern on device_browser (oid text_pattern_ops);
COMMIT;

View File

@@ -0,0 +1,16 @@
BEGIN;
CREATE TABLE snmp_object (
"oid" "text" NOT NULL,
"oid_parts" integer[] NOT NULL,
"mib" "text" NOT NULL,
"leaf" "text" NOT NULL,
"type" "text",
"access" "text",
"index" text[] DEFAULT '{}',
PRIMARY KEY ("oid")
);
CREATE INDEX idx_snmp_object_oid__pattern on snmp_object (oid text_pattern_ops);
COMMIT;

View File

@@ -33,8 +33,8 @@
<input data-form="update" name="ports" type="number" value="[% row.port_count | html_entity %]"> <input data-form="update" name="ports" type="number" value="[% row.port_count | html_entity %]">
</td> </td>
<td class="nd_center-cell"> <td class="nd_center-cell">
<span class="badge">&nbsp;</span><span class="badge">&nbsp;</span> <span class="badge[% ' badge-success' IF row.layers.substr(7,1) %]">[% row.layers.substr(7,1) ? '1' : '&nbsp;' | none %]</span><span class="badge[% ' badge-success' IF row.layers.substr(6,1) %]">[% row.layers.substr(6,1) ? '2' : '&nbsp;' | none %]</span>
<a class="nd_layer-three-link" href="#" rel="tooltip" data-placement="bottom" data-offset="3" data-title="Enable Arpnip"><span class="badge[% ' badge-success' IF row.layers.substr(5,1) %]">3</span></a><span class="badge">&nbsp;</span><span class="badge">&nbsp;</span><span class="badge">&nbsp;</span><span class="badge">&nbsp;</span> <a class="nd_layer-three-link" href="#" rel="tooltip" data-placement="bottom" data-offset="3" data-title="Enable Arpnip"><span class="badge[% ' badge-success' IF row.layers.substr(5,1) %]">3</span></a><span class="badge[% ' badge-success' IF row.layers.substr(4,1) %]">[% row.layers.substr(4,1) ? '4' : '&nbsp;' | none %]</span><span class="badge[% ' badge-success' IF row.layers.substr(3,1) %]">[% row.layers.substr(3,1) ? '5' : '&nbsp;' | none %]</span><span class="badge[% ' badge-success' IF row.layers.substr(2,1) %]">[% row.layers.substr(2,1) ? '6' : '&nbsp;' | none %]</span><span class="badge[% ' badge-success' IF row.layers.substr(1,1) %]">[% row.layers.substr(1,1) ? '7' : '&nbsp;' | none %]</span>
<input data-form="update" name="layers" type="hidden" value="[% row.layers | html_entity %]"> <input data-form="update" name="layers" type="hidden" value="[% row.layers | html_entity %]">
</td> </td>
<td class="nd_center-cell"> <td class="nd_center-cell">

View File

@@ -180,6 +180,24 @@
<input type="hidden" data-form="nbtstat" value="[% d.ip | html_entity %]" name="device"/> <input type="hidden" data-form="nbtstat" value="[% d.ip | html_entity %]" name="device"/>
<button class="btn btn-info btn-small nd_adminbutton" name="nbtstat">NBTstat</button> <button class="btn btn-info btn-small nd_adminbutton" name="nbtstat">NBTstat</button>
[% IF NOT d.is_pseudo %]
<span class="dropdown">
<button class="nd_snap_btn btn [% d.has_snapshot ? 'btn-success' : 'btn-info' %] btn-small dropdown-toggle" type="button" id="snapshotmenu" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Snapshot
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="snapshotmenu">
<input type="hidden" data-form="snapshot_req" value="[% d.ip | html_entity %]" name="device"/>
<li><a href="#" class="nd_adminbutton" name="snapshot_req"><i class="icon-edit"></i> Create</a></li>
<li class='nd_snap_func [% 'disabled' UNLESS d.has_snapshot %]'><a href="[% uri_for('/ajax/content/admin/snapshot_get') | none %]?device=[% d.ip | uri %]"><i class="icon-download-alt"></i> Download</a></li>
<input type="hidden" data-form="snapshot_del" value="[% d.ip | html_entity %]" name="device"/>
<li class='nd_snap_func [% 'disabled' UNLESS d.has_snapshot %]'><a href="#" class="nd_adminbutton" name="snapshot_del"><i class="icon-trash"></i> Delete</a></li>
</ul>
</span>
[% END %]
<button class="btn btn-danger btn-small pull-right" <button class="btn btn-danger btn-small pull-right"
data-toggle="modal" data-target="#nd_devdel" type="button">Delete</button> data-toggle="modal" data-target="#nd_devdel" type="button">Delete</button>

View File

@@ -0,0 +1,132 @@
<div class="row">
<div class="span5">
<div id="jstree" class="nd_scrollable"></div>
</div>
<div id="snmpnodecontainer" class="span8">
<form id="searchTreeForm" class="form-inline col-md-4">
<span class="form-group">
<input id="treeSearchText" type="text" class="form-control" size="30" required placeholder="Search for label or OID">
<label class="checkbox-inline"
rel="tooltip" data-placement="top" data-offset="5" data-title="Anchored to the beginning">
<input type="checkbox" id="snmpPartialSearch" value="partial"> Partial </input>
</label>
</span>
<button type="submit" class="btn btn-default">Search</button>
</form>
<div id="node">
<table class="table table-bordered">
<tbody>
<tr>
<th scope="row" class="span1">OID</th>
<td></td>
</tr>
<tr>
<th scope="row">Module</th>
<td></td>
</tr>
<tr>
<th scope="row">Leaf</th>
<td></td>
</tr>
<tr>
<th scope="row">Type</th>
<td></td>
</tr>
<tr>
<th scope="row">Munge</th>
<td></td>
</tr>
<tr>
<th scope="row">Access</th>
<td></td>
</tr>
<tr>
<th scope="row">Index</th>
<td></td>
</tr>
<tr>
<th scope="row">Value</th>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script type="text/javascript">
$(function () {
var jstree_search_callback = function(str, node) {
var pattern = str.toLowerCase();
var name = node.text.toLowerCase();
var oid = node.id.toLowerCase();
if (document.getElementById('snmpPartialSearch').checked) {
if ((name.indexOf(pattern) == 0) || (oid.indexOf(pattern) == 0)) {
return true;
}
}
else {
if ((name.indexOf(pattern + ' ') == 0) || (oid == pattern)) {
return true;
}
}
return false;
};
$('#jstree').jstree({
'core': {
'multiple' : false,
'themes': {
'name': 'proton',
'responsive': true
},
'data' : {
'url' : function (node) {
return ('[% uri_base | none %]/ajax/data/device/[% device %]/snmptree/'
+ (node.id === '#' ? '.1.3.6.1' : node.id) + '?recurse=on');
}
}
},
'plugins': ['search'],
'search': {
'ajax' : {
'url' : '[% uri_base | none %]/ajax/data/device/[% device %]/snmpnodesearch',
'beforeSend' : function(jqXHR, settings) {
settings.url = settings.url + '&excludeself=on';
if (document.getElementById('snmpPartialSearch').checked) {
settings.url = settings.url + '&partial=on';
}
return true;
}
},
'search_callback' : jstree_search_callback
},
});
$('#snmpnodecontainer').on("change", "#munger", function(e, data) {
var ary = $('#jstree').jstree('get_selected');
$('#node').load('[% uri_base | none %]/ajax/content/device/[% device %]/snmpnode/'
+ ary[0] + '?munge=' + $('#munger').find(":selected").text());
});
$('#jstree').on("changed.jstree", function (e, data) {
if (data.selected && data.selected != "#") {
$('#node').load('[% uri_base | none %]/ajax/content/device/[% device %]/snmpnode/' + data.selected);
}
});
$('#jstree').on("search.jstree", function (e, data) {
if (data.res.length) {
document.getElementById( data.res[0] + '_anchor' ).scrollIntoView();
$('#node').load('[% uri_base | none %]/ajax/content/device/[% device %]/snmpnode/' + data.res[0]);
}
});
$("#searchTreeForm").submit(function(e) {
$("#jstree").jstree("search", $("#treeSearchText").val());
e.preventDefault();
});
$('#treeSearchText').autocomplete({
source: uri_base + '/ajax/data/device/[% device %]/typeahead'
,delay: 150
,minLength: 2
});
});
</script>

View File

@@ -0,0 +1,53 @@
<table class="table table-bordered">
<tbody>
<tr>
<th scope="row" class="span1">OID</th>
<td>[% node.snmp_object.oid %]</td>
</tr>
<tr>
<th scope="row">Module</th>
<td>[% node.snmp_object.mib %]</td>
</tr>
<tr>
<th scope="row">Leaf</th>
<td>[% node.snmp_object.leaf %]</td>
</tr>
<tr>
<th scope="row">Type</th>
<td>[% node.snmp_object.type %]</td>
</tr>
<tr>
<th scope="row">Munge</th>
<td>
[% IF node.value %]
<select name="munger" id="munger">
[% UNLESS munge %]<option value="" selected disabled hidden>None applied</option>[% END %]
[% FOREACH m IN mungers %]
<option [% 'selected' IF m == munge %] value="[% m %]">[% m %]</option>
[% END %]
</select>
[% END %]
</td>
</tr>
<tr>
<th scope="row">Access</th>
<td>[% node.snmp_object.access %]</td>
</tr>
<tr>
<th scope="row">Index</th>
<td>
[% IF node.snmp_object.index.size > 0 %]
<table class="table table-condensed table-bordered">
[% FOREACH idx IN node.snmp_object.index %]
<tr><td>[% idx %]</td></tr>
[% END %]
</table>
[% END %]
</td>
</tr>
<tr>
<th scope="row">Value</th>
<td>[% IF node.value %]<pre id="snmp_node_value">[% node.value %]</pre>[% END %]</td>
</tr>
</tbody>
</table>

View File

@@ -36,10 +36,11 @@
<div class="content"> <div class="content">
<ul id="nd_search-results" class="nav nav-tabs"> <ul id="nd_search-results" class="nav nav-tabs">
[% FOREACH tab IN settings._device_tabs %] [% FOREACH tab IN settings._device_tabs %]
[% NEXT UNLESS tab.render_if %]
<li[% ' class="active"' IF params.tab == tab.tag %]><a id="[% tab.tag | html_entity %]_link" href="#[% tab.tag | html_entity %]_pane">[% tab.label | html_entity %]</a></li> <li[% ' class="active"' IF params.tab == tab.tag %]><a id="[% tab.tag | html_entity %]_link" href="#[% tab.tag | html_entity %]_pane">[% tab.label | html_entity %]</a></li>
[% END %] [% END %]
<span id="nd_device-name"> <span id="nd_device-name">
[% display_name | html_entity %] [% IF is_pseudo %]<span class="badge badge-warning">[% END %][% display_name | html_entity %][% IF is_pseudo %]</span>[% END %]
<a id="nd_csv-download" href="#" download="netdisco.csv">&nbsp; <a id="nd_csv-download" href="#" download="netdisco.csv">&nbsp;
<i id="nd_csv-download-icon" class="text-info icon-file-text-alt icon-large" <i id="nd_csv-download-icon" class="text-info icon-file-text-alt icon-large"
rel="tooltip" data-placement="left" data-offset="5" data-title="Download as CSV"></i></a> rel="tooltip" data-placement="left" data-offset="5" data-title="Download as CSV"></i></a>
@@ -47,6 +48,7 @@
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
[% FOREACH tab IN settings._device_tabs %] [% FOREACH tab IN settings._device_tabs %]
[% NEXT UNLESS tab.render_if %]
<div class="tab-pane[% ' active' IF params.tab == tab.tag %]" id="[% tab.tag | html_entity %]_pane"></div> <div class="tab-pane[% ' active' IF params.tab == tab.tag %]" id="[% tab.tag | html_entity %]_pane"></div>
[% END %] [% END %]
</div> </div>

View File

@@ -169,6 +169,11 @@
,success: function() { ,success: function() {
if (mode != 'delete') { if (mode != 'delete') {
toastr.info('Requested '+ mode +' for device '+ tr.data('for-device')); toastr.info('Requested '+ mode +' for device '+ tr.data('for-device'));
if (mode == 'snapshot_del') {
$('.nd_snap_btn').toggleClass('btn-success');
$('.nd_snap_btn').toggleClass('btn-info');
$('.nd_snap_func').toggleClass('disabled');
}
} }
else { else {
toastr.success('Deleted device '+ tr.data('for-device')); toastr.success('Deleted device '+ tr.data('for-device'));

View File

@@ -32,6 +32,7 @@
<script type="text/javascript" src="[% uri_base | none %]/javascripts/dataTables.bootstrap.js"></script> <script type="text/javascript" src="[% uri_base | none %]/javascripts/dataTables.bootstrap.js"></script>
<script type="text/javascript" src="[% uri_base | none %]/javascripts/dataTables.ip-address-detect.js"></script> <script type="text/javascript" src="[% uri_base | none %]/javascripts/dataTables.ip-address-detect.js"></script>
<script type="text/javascript" src="[% uri_base | none %]/javascripts/dataTables.ip-address-sort.js"></script> <script type="text/javascript" src="[% uri_base | none %]/javascripts/dataTables.ip-address-sort.js"></script>
<script type="text/javascript" src="[% uri_base | none %]/javascripts/jstree/jstree.min.js"></script>
<script type="text/javascript" src="[% uri_base | none %]/javascripts/he.js"></script> <script type="text/javascript" src="[% uri_base | none %]/javascripts/he.js"></script>
<script type="text/javascript" src="[% uri_base | none %]/javascripts/natural.js"></script> <script type="text/javascript" src="[% uri_base | none %]/javascripts/natural.js"></script>
<script type="text/javascript" src="[% uri_base | none %]/javascripts/portsort.js"></script> <script type="text/javascript" src="[% uri_base | none %]/javascripts/portsort.js"></script>
@@ -61,6 +62,7 @@
<link rel="stylesheet" href="[% uri_base | none %]/css/d3-force-network-chart.css"/> <link rel="stylesheet" href="[% uri_base | none %]/css/d3-force-network-chart.css"/>
<link rel="stylesheet" href="[% uri_base | none %]/css/netdisco.css"/> <link rel="stylesheet" href="[% uri_base | none %]/css/netdisco.css"/>
<link rel="stylesheet" href="[% uri_base | none %]/css/bootstrap-tree.css"/> <link rel="stylesheet" href="[% uri_base | none %]/css/bootstrap-tree.css"/>
<link rel="stylesheet" href="[% uri_base | none %]/javascripts/jstree/themes/proton/style.min.css"/>
<link rel="stylesheet" href="[% uri_base | none %]/css/daterangepicker-bs2.css"/> <link rel="stylesheet" href="[% uri_base | none %]/css/daterangepicker-bs2.css"/>
<link rel="stylesheet" href="[% uri_base | none %]/css/dataTables.bootstrap.css"/> <link rel="stylesheet" href="[% uri_base | none %]/css/dataTables.bootstrap.css"/>
<link rel="stylesheet" href="[% uri_base | none %]/css/nd_print.css" media="print"/> <link rel="stylesheet" href="[% uri_base | none %]/css/nd_print.css" media="print"/>