Merge of og-work branch, many new features.
Squashed commit of the following: commita43c98962aAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon Jun 3 20:37:39 2013 +0100 Missing mibdirs causes all MIBs to be loaded (with a warning) commit09829a25b8Author: Oliver Gorwits <oliver@cpan.org> Date: Mon Jun 3 20:07:31 2013 +0100 local plugins site_plugins dir commitb0e804e558Author: Oliver Gorwits <oliver@cpan.org> Date: Mon Jun 3 19:59:04 2013 +0100 use send_error and redirect from Dancer commit3d1185261aAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon Jun 3 19:13:40 2013 +0100 support path config option commit31ca119f84Merge:9a798554d2b3a5Author: Oliver Gorwits <oliver@cpan.org> Date: Mon Jun 3 00:06:17 2013 +0100 Merge remote-tracking branch 'origin/og-work' into og-work g-work" This reverts commit9a79855361, reversing changes made to6fd6118354. Conflicts: Netdisco/share/views/plugin/device_port_column/c_observiumsparklines.tt commit9a79855361Merge:6fd6118c8c3b82Author: Oliver Gorwits <oliver@cpan.org> Date: Mon Jun 3 00:03:32 2013 +0100 Merge remote-tracking branch 'origin/master' into og-work commit6fd6118354Author: Oliver Gorwits <oliver@cpan.org> Date: Sun Jun 2 15:47:45 2013 +0100 extra note about behind proxy commit798086ca29Author: Oliver Gorwits <oliver@cpan.org> Date: Sun Jun 2 15:30:26 2013 +0100 complete the observium plugin commit66b3ced179Author: Oliver Gorwits <oliver@cpan.org> Date: Sun Jun 2 12:48:06 2013 +0100 Plugins can have CSS and Javascript loaded within <head> commit4d2b3a5307Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 08:50:16 2013 +0100 get device dns to port template commited1bfa1ae7Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 08:17:02 2013 +0100 observium sparklines plugin; support X:: namespace commit76b7636c74Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 06:30:06 2013 +0100 rename private settings keys commitfdac8f6c33Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 05:59:53 2013 +0100 add macwalk and arpnip buttons to device details commit3d688c7d83Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 05:57:20 2013 +0100 Revert "reduce refresh to 5sec" This reverts commit8ea9ec7dd9. commitdc62382112Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 05:50:34 2013 +0100 support for arpwalk and macwalk and all jobs via web commit8bc7d83c98Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 30 05:35:41 2013 +0100 simplify discover options to only discoverall and discover commit8ea9ec7dd9Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 20:23:08 2013 +0100 reduce refresh to 5sec commit8c54e6c58bAuthor: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 20:11:06 2013 +0100 show undiscovered neighbor properly commite0ee25628fAuthor: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 19:54:09 2013 +0100 avoid unecessary log for queueing commitd5565423f2Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 19:51:37 2013 +0100 avoid warning on undefined remote type commit5d9b58a6b2Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 19:48:22 2013 +0100 avoid explosion when not admin commit377bb942e0Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 19:46:52 2013 +0100 avoid undefined warning commit08806dcfa2Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 19:46:42 2013 +0100 get_db_version will be 0 at first deploy commit9511c17056Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 29 19:15:55 2013 +0100 fix name of Template module commiteb0288de35Author: Oliver Gorwits <oliver@cpan.org> Date: Tue May 28 07:17:07 2013 +0100 initial config settings documentation commit7f2ea7f8dcAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon May 27 15:18:15 2013 +0100 remove check_mac to own module, use in macsuck too commitb995cf6398Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 27 15:01:29 2013 +0100 show probable but undiscovered neighbor is ports display commitdd8d461188Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 27 14:52:41 2013 +0100 new schema version for is_uplink and is_uplink_admin commit3f6a7b5aa2Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 27 14:47:59 2013 +0100 make sure device_port is updated when manual_topo is set commit33bf9a6599Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 26 19:51:49 2013 +0100 export store_arp and store_node commit0ed356d560Author: Oliver Gorwits <oliver@cpan.org> Date: Sat May 25 17:12:31 2013 +0100 use row lock not table lock commitf830bc3a3bAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat May 25 16:38:33 2013 +0100 move macsuck/arpnip/discover to ::Core namespace commitbe40788987Author: Oliver Gorwits <oliver@cpan.org> Date: Fri May 24 21:10:34 2013 +0100 add maybe_uplink to device_port; more macsuck implementation commit88371026d5Author: Oliver Gorwits <oliver@cpan.org> Date: Fri May 24 14:34:58 2013 +0100 start on macsuck; tweak update locking commit6f7c87ac07Author: Oliver Gorwits <oliver@cpan.org> Date: Fri May 24 13:10:58 2013 +0100 ORDER BY ... FOR UPDATE will allow us to avoid table lock commit7c438e01fcAuthor: Oliver Gorwits <oliver@cpan.org> Date: Fri May 24 12:12:46 2013 +0100 yet more efficient arpnip commitc74c56dc02Author: Oliver Gorwits <oliver@cpan.org> Date: Fri May 24 11:34:23 2013 +0100 guard against race with *_or_* DBIC methods commitd50c54972eAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon May 20 23:42:41 2013 +0100 more efficient arpnip commit73c8979130Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 19 22:52:15 2013 +0100 fix confusing name commitbf78e82411Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 19 22:37:22 2013 +0100 fix mistake in DBIx::Class schema commit6a5af95836Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 19 22:06:27 2013 +0100 arpnip implementation commit594abd3f82Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 16 00:00:50 2013 +0100 PostgreSQL explicit locking support. Squashed commit of the following: commit76e1539102Author: Oliver Gorwits <oliver@cpan.org> Date: Wed May 15 23:54:25 2013 +0100 finished explicit locking module commit369387258bAuthor: Oliver Gorwits <oliver@cpan.org> Date: Tue May 14 23:50:42 2013 +0100 initial implementation of locking from schema object commit55c6d4fe63Author: Oliver Gorwits <oliver@cpan.org> Date: Tue May 14 21:05:01 2013 +0100 add discover button to device details page commit11fd8bf964Author: Oliver Gorwits <oliver@cpan.org> Date: Tue May 14 20:43:43 2013 +0100 fix typo and clear port box on autocomplete dropdown commita00f9b5c2eAuthor: Oliver Gorwits <oliver@cpan.org> Date: Tue May 14 20:38:54 2013 +0100 move admin tasks and remove JobControl package commit74bc0023dfAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat May 11 18:25:04 2013 +0100 complete job queue delete and kill running timers properly when reloading page commitdd6947f38dAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat May 11 16:51:28 2013 +0100 fix improper use of bootstrap table class commitcd5b83f71eAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat May 11 15:55:45 2013 +0100 fix update view icon in sidebar commite9349f325dAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat May 11 11:57:19 2013 +0100 css audit commit201470275dAuthor: Oliver Gorwits <oliver@cpan.org> Date: Thu May 9 23:48:05 2013 +0100 add job queue to standard plugins list commita18a3c72a3Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 9 23:37:43 2013 +0100 fix table headings and improve Action display in Job Queue commit70f5da8bb6Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 9 23:30:32 2013 +0100 implement "no devices" prompt for admin users to do first discover commit2e8ac83173Author: Oliver Gorwits <oliver@cpan.org> Date: Thu May 9 21:53:39 2013 +0100 more js refactoring for report and search commit479ac0e55dAuthor: Oliver Gorwits <oliver@cpan.org> Date: Thu May 9 21:50:29 2013 +0100 refactor js for device tabs commit6a17fe5d6cAuthor: Oliver Gorwits <oliver@cpan.org> Date: Thu May 9 21:05:42 2013 +0100 fix crazy races with javasacript by using global delegations commite94e3cef3bAuthor: Oliver Gorwits <oliver@cpan.org> Date: Wed May 8 23:06:41 2013 +0100 remove Try::Tiny from web runtime commitc746e68b9bAuthor: Oliver Gorwits <oliver@cpan.org> Date: Tue May 7 21:54:11 2013 +0100 make topo autocomplete more responsive commit24c511786fAuthor: Oliver Gorwits <oliver@cpan.org> Date: Tue May 7 21:52:17 2013 +0100 display name and IP for device typeahead commit52ab7d1266Author: Oliver Gorwits <oliver@cpan.org> Date: Tue May 7 21:47:05 2013 +0100 add drop-down control for the topo form fields commit5744b6845fAuthor: Oliver Gorwits <oliver@cpan.org> Date: Tue May 7 21:25:30 2013 +0100 complete the topology editor (add/delete) commitb510fbe8c5Author: Oliver Gorwits <oliver@cpan.org> Date: Tue May 7 00:59:11 2013 +0100 add new admin tasks to default plugins list commit11d55e0129Author: Oliver Gorwits <oliver@cpan.org> Date: Tue May 7 00:56:19 2013 +0100 Manual Device Topology Needed to add the 'autocomplete' jQuery UI component because it can do minLength=0 properly. Used the smoothness UI theme. Added typeahead AJAX calls to support the topology searching. Added new plugin and template for the topology editing page. commitbf7a419d08Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 6 22:16:24 2013 +0100 add a little colour to lone tab titles commit9690a31f19Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 6 22:01:13 2013 +0100 complete Manage Pseudo Devices commit024f4d9a83Author: Oliver Gorwits <oliver@cpan.org> Date: Mon May 6 00:49:47 2013 +0100 use bootstrap font colour instead of css commitf75f1e5cbfAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon May 6 00:45:18 2013 +0100 add frontend update/del forms, and display port count commitf0899e16b3Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 23:53:20 2013 +0100 add frontend pseudo device add form commit3271c01931Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 21:45:17 2013 +0100 complete the code for admin tasks page loading commit38f70624f3Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 17:04:30 2013 +0100 set up file paths consistently in all scripts commitc761ca839bAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 17:00:30 2013 +0100 Helper script to import the Netdisco 1.x Topology file to the database commitf468b48049Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 16:20:39 2013 +0100 Handle whitespace ahead of OUI data commit5c8a5754f6Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 16:16:20 2013 +0100 also set neighbor info when discovering device interfaces commitacb988b6afAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 15:34:20 2013 +0100 try to avoid duplicate execution of scheduled jobs commitc6bcaf66c5Author: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 14:16:25 2013 +0100 do not clobber manual topo when discovering neighbors commitd9a6a1882aAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sun May 5 13:02:45 2013 +0100 User icon color indicates port_control/admin ability commit2cdcb9db7eAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon Apr 29 23:34:27 2013 +0100 add support for admin tasks as plugins commit075a770c9aAuthor: Oliver Gorwits <oliver@cpan.org> Date: Mon Apr 29 22:23:20 2013 +0100 skip pseudo devices (vendor netdisco) commit045c022d42Author: Oliver Gorwits <oliver@cpan.org> Date: Mon Apr 29 21:58:33 2013 +0100 incorporate manual topo info from the topology db table commit09285d42b4Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 18:39:12 2013 +0100 add unique constraints to topology table commit2780b72e49Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 15:38:05 2013 +0100 muted help text in sidebar commit733d4f83fbAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:39:54 2013 +0100 sorry, testing hook changes commit71e366e352Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:34:36 2013 +0100 sorry, testing hook changes commit7f9eaa99f5Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:33:44 2013 +0100 sorry, testing hook changes commit5215fd632dAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:30:07 2013 +0100 sorry, testing hook changes commitbe817d60c2Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:21:45 2013 +0100 sorry, testing hook changes commit1fd3695358Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:18:57 2013 +0100 sorry, testing hook changes commitac448c4a91Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:13:03 2013 +0100 sorry, testing hook changes commitc563b8d9afAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:08:54 2013 +0100 sorry, testing hook changes commit3abcfb01d5Author: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:06:25 2013 +0100 sorry, testing hook changes commit877a81facfAuthor: Oliver Gorwits <oliver@cpan.org> Date: Sat Apr 27 14:05:25 2013 +0100 sorry, testing hook changes
This commit is contained in:
176
Netdisco/lib/App/Netdisco/Core/Arpnip.pm
Normal file
176
Netdisco/lib/App/Netdisco/Core/Arpnip.pm
Normal file
@@ -0,0 +1,176 @@
|
||||
package App::Netdisco::Core::Arpnip;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::PortMAC 'get_port_macs';
|
||||
use App::Netdisco::Util::SanityCheck 'check_mac';
|
||||
use App::Netdisco::Util::DNS ':all';
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
use Time::HiRes 'gettimeofday';
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/ do_arpnip store_arp /;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Core::Arpnip
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Helper subroutines to support parts of the Netdisco application.
|
||||
|
||||
There are no default exports, however the C<:all> tag will export all
|
||||
subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 do_arpnip( $device, $snmp )
|
||||
|
||||
Given a Device database object, and a working SNMP connection, connect to a
|
||||
device and discover its ARP cache for IPv4 and Neighbor cache for IPv6.
|
||||
|
||||
Will also discover subnets in use on the device and update the Subnets table.
|
||||
|
||||
=cut
|
||||
|
||||
sub do_arpnip {
|
||||
my ($device, $snmp) = @_;
|
||||
|
||||
unless ($device->in_storage) {
|
||||
debug sprintf ' [%s] arpnip - skipping device not yet discovered', $device->ip;
|
||||
return;
|
||||
}
|
||||
|
||||
my $port_macs = get_port_macs($device);
|
||||
|
||||
# get v4 arp table
|
||||
my @v4 = _get_arps($device, $port_macs, $snmp->at_paddr, $snmp->at_netaddr);
|
||||
# get v6 neighbor cache
|
||||
my @v6 = _get_arps($device, $port_macs, $snmp->ipv6_n2p_mac, $snmp->ipv6_n2p_addr);
|
||||
|
||||
# get directly connected networks
|
||||
my @subnets = _gather_subnets($device, $snmp);
|
||||
# TODO: IPv6 subnets
|
||||
|
||||
# would be possible just to use now() on updated records, but by using this
|
||||
# same value for them all, we _can_ if we want add a job at the end to
|
||||
# select and do something with the updated set (no reason to yet, though)
|
||||
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
|
||||
|
||||
# update node_ip with ARP and Neighbor Cache entries
|
||||
store_arp(@$_, $now) for @v4;
|
||||
debug sprintf ' [%s] arpnip - processed %s ARP Cache entries',
|
||||
$device->ip, scalar @v4;
|
||||
|
||||
store_arp(@$_, $now) for @v6;
|
||||
debug sprintf ' [%s] arpnip - processed %s IPv6 Neighbor Cache entries',
|
||||
$device->ip, scalar @v6;
|
||||
|
||||
_store_subnet($_, $now) for @subnets;
|
||||
debug sprintf ' [%s] arpnip - processed %s Subnet entries',
|
||||
$device->ip, scalar @subnets;
|
||||
}
|
||||
|
||||
# get an arp table (v4 or v6)
|
||||
sub _get_arps {
|
||||
my ($device, $port_macs, $paddr, $netaddr) = @_;
|
||||
my @arps = ();
|
||||
|
||||
while (my ($arp, $node) = each %$paddr) {
|
||||
my $ip = $netaddr->{$arp};
|
||||
next unless defined $ip;
|
||||
next unless check_mac($device, $node, $port_macs);
|
||||
push @arps, [$node, $ip, hostname_from_ip($ip)];
|
||||
}
|
||||
|
||||
return @arps;
|
||||
}
|
||||
|
||||
=head2 store_arp( $mac, $ip, $name, $now? )
|
||||
|
||||
Stores a new entry to the C<node_ip> table with the given MAC, IP (v4 or v6)
|
||||
and DNS host name.
|
||||
|
||||
Will mark old entries for this IP as no longer C<active>.
|
||||
|
||||
Optionally a literal string can be passed in the fourth argument for the
|
||||
C<time_last> timestamp, otherwise the current timestamp (C<now()>) is used.
|
||||
|
||||
=cut
|
||||
|
||||
sub store_arp {
|
||||
my ($mac, $ip, $name, $now) = @_;
|
||||
$now ||= 'now()';
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $current = schema('netdisco')->resultset('NodeIp')
|
||||
->search({ip => $ip, -bool => 'active'})
|
||||
->search(undef, {
|
||||
columns => [qw/mac ip/],
|
||||
order_by => [qw/mac ip/],
|
||||
for => 'update'
|
||||
});
|
||||
$current->first; # lock rows
|
||||
$current->update({active => \'false'});
|
||||
|
||||
schema('netdisco')->resultset('NodeIp')
|
||||
->search({'me.mac' => $mac, 'me.ip' => $ip})
|
||||
->update_or_create(
|
||||
{
|
||||
dns => $name,
|
||||
active => \'true',
|
||||
time_last => \$now,
|
||||
},
|
||||
{
|
||||
order_by => [qw/mac ip/],
|
||||
for => 'update',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
# gathers device subnets
|
||||
sub _gather_subnets {
|
||||
my ($device, $snmp) = @_;
|
||||
my @subnets = ();
|
||||
|
||||
my $ip_netmask = $snmp->ip_netmask;
|
||||
my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8');
|
||||
|
||||
foreach my $entry (keys %$ip_netmask) {
|
||||
my $ip = NetAddr::IP::Lite->new($entry);
|
||||
my $addr = $ip->addr;
|
||||
|
||||
next if $addr eq '0.0.0.0';
|
||||
next if $ip->within($localnet);
|
||||
next if setting('ignore_private_nets') and $ip->is_rfc1918;
|
||||
|
||||
my $netmask = $ip_netmask->{$addr};
|
||||
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;
|
||||
|
||||
debug sprintf ' [%s] arpnip - found subnet %s', $device->ip, $cidr;
|
||||
push @subnets, $cidr;
|
||||
}
|
||||
|
||||
return @subnets;
|
||||
}
|
||||
|
||||
# update subnets with new networks
|
||||
sub _store_subnet {
|
||||
my ($subnet, $now) = @_;
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
schema('netdisco')->resultset('Subnet')->update_or_create(
|
||||
{
|
||||
net => $subnet,
|
||||
last_discover => \$now,
|
||||
},
|
||||
{ for => 'update' });
|
||||
});
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -1,4 +1,4 @@
|
||||
package App::Netdisco::Util::DiscoverAndStore;
|
||||
package App::Netdisco::Core::Discover;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
@@ -13,13 +13,13 @@ our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/
|
||||
store_device store_interfaces store_wireless
|
||||
store_vlans store_power store_modules
|
||||
find_neighbors
|
||||
store_neighbors discover_new_neighbors
|
||||
/;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Util::DiscoverAndStore
|
||||
App::Netdisco::Core::Discover
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
@@ -52,6 +52,7 @@ sub store_device {
|
||||
|
||||
my $hostname = hostname_from_ip($device->ip);
|
||||
$device->dns($hostname) if length $hostname;
|
||||
my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8');
|
||||
|
||||
# build device aliases suitable for DBIC
|
||||
my @aliases;
|
||||
@@ -60,7 +61,7 @@ sub store_device {
|
||||
my $addr = $ip->addr;
|
||||
|
||||
next if $addr eq '0.0.0.0';
|
||||
next if $ip->within(NetAddr::IP::Lite->new('127.0.0.0/8'));
|
||||
next if $ip->within($localnet);
|
||||
next if setting('ignore_private_nets') and $ip->is_rfc1918;
|
||||
|
||||
my $iid = $ip_index->{$addr};
|
||||
@@ -105,7 +106,7 @@ sub store_device {
|
||||
my $gone = $device->device_ips->delete;
|
||||
debug sprintf ' [%s] device - removed %s aliases',
|
||||
$device->ip, $gone;
|
||||
$device->update_or_insert;
|
||||
$device->update_or_insert(undef, {for => 'update'});
|
||||
$device->device_ips->populate(\@aliases);
|
||||
debug sprintf ' [%s] device - added %d new aliases',
|
||||
$device->ip, scalar @aliases;
|
||||
@@ -118,7 +119,7 @@ sub _set_canonical_ip {
|
||||
my $oldip = $device->ip;
|
||||
my $newip = $snmp->root_ip;
|
||||
|
||||
if (length $newip) {
|
||||
if (defined $newip) {
|
||||
if ($oldip ne $newip) {
|
||||
debug sprintf ' [%s] device - changing root IP to alt IP %s',
|
||||
$oldip, $newip;
|
||||
@@ -252,7 +253,7 @@ sub store_interfaces {
|
||||
my $gone = $device->ports->delete;
|
||||
debug sprintf ' [%s] interfaces - removed %s interfaces',
|
||||
$device->ip, $gone;
|
||||
$device->update_or_insert;
|
||||
$device->update_or_insert(undef, {for => 'update'});
|
||||
$device->ports->populate(\@interfaces);
|
||||
debug sprintf ' [%s] interfaces - added %d new interfaces',
|
||||
$device->ip, scalar @interfaces;
|
||||
@@ -562,26 +563,34 @@ sub store_modules {
|
||||
});
|
||||
}
|
||||
|
||||
=head2 find_neighbors( $device, $snmp )
|
||||
=head2 store_neighbors( $device, $snmp )
|
||||
|
||||
returns: C<@to_discover>
|
||||
|
||||
Given a Device database object, and a working SNMP connection, discover and
|
||||
store the device's port neighbors information.
|
||||
|
||||
If any neighbor is unknown to Netdisco, a discover job for it will immediately
|
||||
be queued (modulo configuration file C<discover_no_type> setting).
|
||||
Entries in the Topology database table will override any discovered device
|
||||
port relationships.
|
||||
|
||||
The Device database object can be a fresh L<DBIx::Class::Row> object which is
|
||||
not yet stored to the database.
|
||||
|
||||
A list of discovererd neighbors will be returned as [C<$ip>, C<$type>] tuples.
|
||||
|
||||
=cut
|
||||
|
||||
sub find_neighbors {
|
||||
sub store_neighbors {
|
||||
my ($device, $snmp) = @_;
|
||||
my @to_discover = ();
|
||||
|
||||
# first allow any manually configred topology to be set
|
||||
_set_manual_topology($device, $snmp);
|
||||
|
||||
my $c_ip = $snmp->c_ip;
|
||||
unless ($snmp->hasCDP or scalar keys %$c_ip) {
|
||||
debug sprintf ' [%s] neigh - CDP/LLDP not enabled!', $device->ip;
|
||||
return;
|
||||
return @to_discover;
|
||||
}
|
||||
|
||||
my $interfaces = $snmp->interfaces;
|
||||
@@ -642,17 +651,28 @@ sub find_neighbors {
|
||||
}
|
||||
}
|
||||
|
||||
# IP Phone detection type fixup
|
||||
if (defined $remote_type and $remote_type =~ m/(mitel.5\d{3})/i) {
|
||||
$remote_type = 'IP Phone - '. $remote_type
|
||||
if $remote_type !~ /ip phone/i;
|
||||
}
|
||||
else {
|
||||
$remote_type = '';
|
||||
}
|
||||
|
||||
# hack for devices seeing multiple neighbors on the port
|
||||
if (ref [] eq ref $remote_ip) {
|
||||
debug sprintf
|
||||
' [%s] neigh - port %s has multiple neighbors, setting remote as self',
|
||||
$device->ip, $port;
|
||||
|
||||
foreach my $n (@$remote_ip) {
|
||||
debug sprintf
|
||||
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
|
||||
$device->ip, $n, $remote_type, $port;
|
||||
_enqueue_discover($n, $remote_type);
|
||||
if (wantarray) {
|
||||
foreach my $n (@$remote_ip) {
|
||||
debug sprintf
|
||||
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
|
||||
$device->ip, $n, $remote_type, $port;
|
||||
push @to_discover, [$n, $remote_type];
|
||||
}
|
||||
}
|
||||
|
||||
# set self as remote IP to suppress any further work
|
||||
@@ -660,6 +680,14 @@ sub find_neighbors {
|
||||
$remote_port = $port;
|
||||
}
|
||||
else {
|
||||
# what we came here to do.... discover the neighbor
|
||||
if (wantarray) {
|
||||
debug sprintf
|
||||
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
|
||||
$device->ip, $remote_ip, $remote_type, $port;
|
||||
push @to_discover, [$remote_ip, $remote_type];
|
||||
}
|
||||
|
||||
$remote_port = $c_port->{$entry};
|
||||
|
||||
if (defined $remote_port) {
|
||||
@@ -672,12 +700,7 @@ sub find_neighbors {
|
||||
}
|
||||
}
|
||||
|
||||
# XXX too custom? IP Phone detection
|
||||
if (defined $remote_type and $remote_type =~ m/(mitel.5\d{3})/i) {
|
||||
$remote_type = 'IP Phone - '. $remote_type
|
||||
if $remote_type !~ /ip phone/i;
|
||||
}
|
||||
|
||||
# if all the data looks sane, update the port row with neighbor info
|
||||
my $portrow = schema('netdisco')->resultset('DevicePort')
|
||||
->single({ip => $device->ip, port => $port});
|
||||
|
||||
@@ -687,44 +710,120 @@ sub find_neighbors {
|
||||
next;
|
||||
}
|
||||
|
||||
if ($portrow->manual_topo) {
|
||||
info sprintf ' [%s] neigh - %s has manually defined topology',
|
||||
$device->ip, $port;
|
||||
next;
|
||||
}
|
||||
|
||||
$portrow->update({
|
||||
remote_ip => $remote_ip,
|
||||
remote_port => $remote_port,
|
||||
remote_type => $remote_type,
|
||||
remote_id => $remote_id,
|
||||
is_uplink => \"true",
|
||||
manual_topo => \"false",
|
||||
});
|
||||
|
||||
debug sprintf
|
||||
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
|
||||
$device->ip, $remote_ip, $remote_type, $port;
|
||||
_enqueue_discover($remote_ip, $remote_type);
|
||||
}
|
||||
|
||||
return @to_discover;
|
||||
}
|
||||
|
||||
# only enqueue if device is not already discovered, and
|
||||
# discover_no_type config permits the discovery
|
||||
sub _enqueue_discover {
|
||||
my ($ip, $remote_type) = @_;
|
||||
# take data from the topology table and update remote_ip and remote_port
|
||||
# in the devices table. only use root_ips and skip any bad topo entries.
|
||||
sub _set_manual_topology {
|
||||
my ($device, $snmp) = @_;
|
||||
|
||||
my $device = get_device($ip);
|
||||
return if $device->in_storage;
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# clear manual topology flags
|
||||
schema('netdisco')->resultset('DevicePort')->update({manual_topo => \'false'});
|
||||
|
||||
my $remote_type_match = setting('discover_no_type');
|
||||
if ($remote_type and $remote_type_match
|
||||
and $remote_type =~ m/$remote_type_match/) {
|
||||
debug sprintf ' queue - %s, type [%s] excluded by discover_no_type',
|
||||
$ip, $remote_type;
|
||||
return;
|
||||
}
|
||||
my $topo_links = schema('netdisco')->resultset('Topology');
|
||||
debug sprintf ' [%s] neigh - setting manual topology links', $device->ip;
|
||||
|
||||
while (my $link = $topo_links->next) {
|
||||
# could fail for broken topo, but we ignore to try the rest
|
||||
try {
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# only work on root_ips
|
||||
my $left = get_device($link->dev1);
|
||||
my $right = get_device($link->dev2);
|
||||
|
||||
# skip bad entries
|
||||
return unless ($left->in_storage and $right->in_storage);
|
||||
|
||||
$left->ports
|
||||
->single({port => $link->port1}, {for => 'update'})
|
||||
->update({
|
||||
remote_ip => $right->ip,
|
||||
remote_port => $link->port2,
|
||||
remote_type => undef,
|
||||
remote_id => undef,
|
||||
is_uplink => \"true",
|
||||
manual_topo => \"true",
|
||||
});
|
||||
|
||||
$right->ports
|
||||
->single({port => $link->port2}, {for => 'update'})
|
||||
->update({
|
||||
remote_ip => $left->ip,
|
||||
remote_port => $link->port1,
|
||||
remote_type => undef,
|
||||
remote_id => undef,
|
||||
is_uplink => \"true",
|
||||
manual_topo => \"true",
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
=head2 discover_new_neighbors( $device, $snmp )
|
||||
|
||||
Given a Device database object, and a working SNMP connection, discover and
|
||||
store the device's port neighbors information.
|
||||
|
||||
Entries in the Topology database table will override any discovered device
|
||||
port relationships.
|
||||
|
||||
The Device database object can be a fresh L<DBIx::Class::Row> object which is
|
||||
not yet stored to the database.
|
||||
|
||||
Any discovered neighbor unknown to Netdisco will have a C<discover> job
|
||||
immediately queued (subject to the filtering by the C<discover_no_type>
|
||||
setting).
|
||||
|
||||
=cut
|
||||
|
||||
sub discover_new_neighbors {
|
||||
my @to_discover = store_neighbors(@_);
|
||||
|
||||
# only enqueue if device is not already discovered, and
|
||||
# discover_no_type config permits the discovery
|
||||
foreach my $neighbor (@to_discover) {
|
||||
my ($ip, $remote_type) = @$neighbor;
|
||||
|
||||
my $device = get_device($ip);
|
||||
next if $device->in_storage;
|
||||
|
||||
my $remote_type_match = setting('discover_no_type');
|
||||
if ($remote_type and $remote_type_match
|
||||
and $remote_type =~ m/$remote_type_match/) {
|
||||
debug sprintf ' queue - %s, type [%s] excluded by discover_no_type',
|
||||
$ip, $remote_type;
|
||||
next;
|
||||
}
|
||||
|
||||
try {
|
||||
# could fail if queued job already exists
|
||||
schema('netdisco')->resultset('Admin')->create({
|
||||
device => $ip,
|
||||
action => 'discover',
|
||||
status => 'queued',
|
||||
});
|
||||
};
|
||||
try {
|
||||
schema('netdisco')->resultset('Admin')->create({
|
||||
device => $ip,
|
||||
action => 'discover',
|
||||
status => 'queued',
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
457
Netdisco/lib/App/Netdisco/Core/Macsuck.pm
Normal file
457
Netdisco/lib/App/Netdisco/Core/Macsuck.pm
Normal file
@@ -0,0 +1,457 @@
|
||||
package App::Netdisco::Core::Macsuck;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::PortMAC 'get_port_macs';
|
||||
use App::Netdisco::Util::SanityCheck 'check_mac';
|
||||
use App::Netdisco::Util::SNMP 'snmp_comm_reindex';
|
||||
use Time::HiRes 'gettimeofday';
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/
|
||||
do_macsuck
|
||||
store_node
|
||||
store_wireless_client_info
|
||||
/;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Core::Macsuck
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Helper subroutines to support parts of the Netdisco application.
|
||||
|
||||
There are no default exports, however the C<:all> tag will export all
|
||||
subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 do_macsuck( $device, $snmp )
|
||||
|
||||
Given a Device database object, and a working SNMP connection, connect to a
|
||||
device and discover the MAC addresses listed against each physical port
|
||||
without a neighbor.
|
||||
|
||||
If the device has VLANs, C<do_macsuck> will walk each VALN to get the MAC
|
||||
addresses from there.
|
||||
|
||||
It will also gather wireless client information if C<store_wireless_client>
|
||||
configuration setting is enabled.
|
||||
|
||||
=cut
|
||||
|
||||
sub do_macsuck {
|
||||
my ($device, $snmp) = @_;
|
||||
|
||||
unless ($device->in_storage) {
|
||||
debug sprintf
|
||||
' [%s] macsuck - skipping device not yet discovered',
|
||||
$device->ip;
|
||||
return;
|
||||
}
|
||||
|
||||
# would be possible just to use now() on updated records, but by using this
|
||||
# same value for them all, we _can_ if we want add a job at the end to
|
||||
# select and do something with the updated set (no reason to yet, though)
|
||||
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
|
||||
my $total_nodes = 0;
|
||||
|
||||
# do this before we start messing with the snmp community string
|
||||
store_wireless_client_info($device, $snmp, $now);
|
||||
|
||||
# cache the device ports to save hitting the database for many single rows
|
||||
my $device_ports = {map {($_->port => $_)} $device->ports->all};
|
||||
my $port_macs = get_port_macs($device);
|
||||
|
||||
# get forwarding table data via basic snmp connection
|
||||
my $fwtable = { 0 => _walk_fwtable($device, $snmp, $port_macs, $device_ports) };
|
||||
|
||||
# ...then per-vlan if supported
|
||||
my @vlan_list = _get_vlan_list($device, $snmp);
|
||||
foreach my $vlan (@vlan_list) {
|
||||
snmp_comm_reindex($snmp, $vlan);
|
||||
$fwtable->{$vlan} = _walk_fwtable($device, $snmp, $port_macs, $device_ports);
|
||||
}
|
||||
|
||||
# now it's time to call store_node for every node discovered
|
||||
# on every port on every vlan on this device.
|
||||
|
||||
# reverse sort allows vlan 0 entries to be included only as fallback
|
||||
foreach my $vlan (reverse sort keys %$fwtable) {
|
||||
foreach my $port (keys %{ $fwtable->{$vlan} }) {
|
||||
if ($device_ports->{$port}->is_uplink) {
|
||||
debug sprintf
|
||||
' [%s] macsuck - port %s is uplink, topo broken - skipping.',
|
||||
$device->ip, $port;
|
||||
next;
|
||||
}
|
||||
|
||||
debug sprintf ' [%s] macsuck - port %s vlan %s : %s nodes',
|
||||
$device->ip, $port, $vlan, scalar keys %{ $fwtable->{$vlan}->{$port} };
|
||||
|
||||
foreach my $mac (keys %{ $fwtable->{$vlan}->{$port} }) {
|
||||
# remove vlan 0 entry for this MAC addr
|
||||
delete $fwtable->{0}->{$_}->{$mac}
|
||||
for keys %{ $fwtable->{0} };
|
||||
|
||||
++$total_nodes;
|
||||
store_node($device->ip, $vlan, $port, $mac, $now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debug sprintf ' [%s] macsuck - %s forwarding table entries',
|
||||
$device->ip, $total_nodes;
|
||||
$device->update({last_macsuck => \$now});
|
||||
}
|
||||
|
||||
=head2 store_node( $ip, $vlan, $port, $mac, $now? )
|
||||
|
||||
Writes a fresh entry to the Netdisco C<node> database table. Will mark old
|
||||
entries for this data as no longer C<active>.
|
||||
|
||||
All four fields in the tuple are required. If you don't know the VLAN ID,
|
||||
Netdisco supports using ID "0".
|
||||
|
||||
Optionally, a fifth argument can be the literal string passed to the time_last
|
||||
field of the database record. If not provided, it defauls to C<now()>.
|
||||
|
||||
=cut
|
||||
|
||||
sub store_node {
|
||||
my ($ip, $vlan, $port, $mac, $now) = @_;
|
||||
$now ||= 'now()';
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $nodes = schema('netdisco')->resultset('Node');
|
||||
|
||||
# TODO: probably needs changing if we're to support VTP domains
|
||||
my $old = $nodes->search(
|
||||
{
|
||||
mac => $mac,
|
||||
vlan => $vlan,
|
||||
-bool => 'active',
|
||||
-not => {
|
||||
switch => $ip,
|
||||
port => $port,
|
||||
},
|
||||
});
|
||||
|
||||
# lock rows,
|
||||
# and get the count so we know whether to set time_recent
|
||||
my $old_count = scalar $old->search(undef,
|
||||
{
|
||||
columns => [qw/switch vlan port mac/],
|
||||
order_by => [qw/switch vlan port mac/],
|
||||
for => 'update',
|
||||
})->all;
|
||||
|
||||
$old->update({ active => \'false' });
|
||||
|
||||
my $new = $nodes->search(
|
||||
{
|
||||
'me.switch' => $ip,
|
||||
'me.port' => $port,
|
||||
'me.mac' => $mac,
|
||||
},
|
||||
{
|
||||
order_by => [qw/switch vlan port mac/],
|
||||
for => 'update',
|
||||
});
|
||||
|
||||
# lock rows
|
||||
$new->search({vlan => [$vlan, 0, undef]})->first;
|
||||
|
||||
# upgrade old schema
|
||||
$new->search({vlan => [$vlan, 0, undef]})
|
||||
->update({vlan => $vlan});
|
||||
|
||||
$new->update_or_create({
|
||||
vlan => $vlan,
|
||||
active => \'true',
|
||||
oui => substr($mac,0,8),
|
||||
time_last => \$now,
|
||||
($old_count ? (time_recent => \$now) : ()),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
# return a list of vlan numbers which are OK to macsuck on this device
|
||||
sub _get_vlan_list {
|
||||
my ($device, $snmp) = @_;
|
||||
|
||||
return () unless $snmp->cisco_comm_indexing;
|
||||
|
||||
my (%vlans, %vlan_names);
|
||||
my $i_vlan = $snmp->i_vlan || {};
|
||||
|
||||
# get list of vlans in use
|
||||
while (my ($idx, $vlan) = each %$i_vlan) {
|
||||
# hack: if vlan id comes as 1.142 instead of 142
|
||||
$vlan =~ s/^\d+\.//;
|
||||
|
||||
++$vlans{$vlan};
|
||||
}
|
||||
|
||||
unless (scalar keys %vlans) {
|
||||
debug sprintf ' [%s] macsuck - no VLANs found.', $device->ip;
|
||||
return ();
|
||||
}
|
||||
|
||||
my $v_name = $snmp->v_name || {};
|
||||
|
||||
# get vlan names (required for config which filters by name)
|
||||
while (my ($idx, $name) = each %$v_name) {
|
||||
# hack: if vlan id comes as 1.142 instead of 142
|
||||
(my $vlan = $idx) =~ s/^\d+\.//;
|
||||
|
||||
# just in case i_vlan is different to v_name set
|
||||
++$vlans{$vlan};
|
||||
|
||||
$vlan_names{$vlan} = $name;
|
||||
}
|
||||
|
||||
debug sprintf ' [%s] macsuck - VLANs: %s', $device->ip,
|
||||
(join ',', sort keys %vlans);
|
||||
|
||||
my @ok_vlans = ();
|
||||
foreach my $vlan (sort keys %vlans) {
|
||||
my $name = $vlan_names{$vlan} || '(unnamed)';
|
||||
|
||||
# FIXME: macsuck_no_vlan
|
||||
# FIXME: macsuck_no_devicevlan
|
||||
|
||||
if (setting('macsuck_no_unnamed') and $name eq '(unnamed)') {
|
||||
debug sprintf
|
||||
' [%s] macsuck VLAN %s - skipped by macsuck_no_unnamed config',
|
||||
$device->ip, $vlan;
|
||||
next;
|
||||
}
|
||||
|
||||
if ($vlan == 0 or $vlan > 4094) {
|
||||
debug sprintf ' [%s] macsuck - invalid VLAN number %s',
|
||||
$device->ip, $vlan;
|
||||
next;
|
||||
}
|
||||
|
||||
# check in use by a port on this device
|
||||
if (scalar keys %$i_vlan and not exists $vlans{$vlan}
|
||||
and not setting('macsuck_all_vlans')) {
|
||||
|
||||
debug sprintf
|
||||
' [%s] macsuck VLAN %s/%s - not in use by any port - skipping.',
|
||||
$device->ip, $vlan, $name;
|
||||
next;
|
||||
}
|
||||
|
||||
push @ok_vlans, $vlan;
|
||||
}
|
||||
|
||||
return @ok_vlans;
|
||||
}
|
||||
|
||||
# walks the forwarding table (BRIDGE-MIB) for the device and returns a
|
||||
# table of node entries.
|
||||
sub _walk_fwtable {
|
||||
my ($device, $snmp, $port_macs, $device_ports) = @_;
|
||||
my $cache = {};
|
||||
|
||||
my $fw_mac = $snmp->fw_mac;
|
||||
my $fw_port = $snmp->fw_port;
|
||||
my $fw_vlan = $snmp->qb_fw_vlan;
|
||||
my $bp_index = $snmp->bp_index;
|
||||
my $interfaces = $snmp->interfaces;
|
||||
|
||||
# to map forwarding table port to device port we have
|
||||
# fw_port -> bp_index -> interfaces
|
||||
|
||||
while (my ($idx, $mac) = each %$fw_mac) {
|
||||
my $bp_id = $fw_port->{$idx};
|
||||
next unless check_mac($device, $mac, $port_macs);
|
||||
|
||||
unless (defined $bp_id) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - %s has no fw_port mapping - skipping.',
|
||||
$device->ip, $mac, $idx;
|
||||
next;
|
||||
}
|
||||
|
||||
my $iid = $bp_index->{$bp_id};
|
||||
|
||||
unless (defined $iid) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s has no bp_index mapping - skipping.',
|
||||
$device->ip, $mac, $bp_id;
|
||||
next;
|
||||
}
|
||||
|
||||
my $port = $interfaces->{$iid};
|
||||
|
||||
unless (defined $port) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - iid %s has no port mapping - skipping.',
|
||||
$device->ip, $mac, $iid;
|
||||
next;
|
||||
}
|
||||
|
||||
# TODO: add proper port channel support!
|
||||
if ($port =~ m/port.channel/i) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s is LAG member - skipping.',
|
||||
$device->ip, $mac, $port;
|
||||
next;
|
||||
}
|
||||
|
||||
# this uses the cached $ports resultset to limit hits on the db
|
||||
my $device_port = $device_ports->{$port};
|
||||
|
||||
unless (defined $device_port) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s is not in database - skipping.',
|
||||
$device->ip, $mac, $port;
|
||||
next;
|
||||
}
|
||||
|
||||
# check to see if the port is connected to another device
|
||||
# and if we have that device in the database.
|
||||
|
||||
# we have several ways to detect "uplink" port status:
|
||||
# * a neighbor was discovered using CDP/LLDP
|
||||
# * a mac addr is seen which belongs to any device port/interface
|
||||
# * (TODO) admin sets is_uplink_admin on the device_port
|
||||
|
||||
if ($device_port->is_uplink) {
|
||||
if (my $neighbor = $device_port->neighbor) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s has neighbor %s - skipping.',
|
||||
$device->ip, $mac, $port, $neighbor->ip;
|
||||
next;
|
||||
}
|
||||
elsif (my $remote = $device_port->remote_ip) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s has undiscovered neighbor %s',
|
||||
$device->ip, $mac, $port, $remote;
|
||||
# continue!!
|
||||
}
|
||||
else {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s is detected uplink - skipping.',
|
||||
$device->ip, $mac, $port;
|
||||
next;
|
||||
}
|
||||
}
|
||||
|
||||
if (exists $port_macs->{$mac}) {
|
||||
my $switch_ip = $port_macs->{$mac};
|
||||
if ($device->ip eq $switch_ip) {
|
||||
debug sprintf
|
||||
' [%s] macsuck %s - port %s connects to self - skipping.',
|
||||
$device->ip, $mac, $port;
|
||||
next;
|
||||
}
|
||||
|
||||
debug sprintf ' [%s] macsuck %s - port %s is probably an uplink',
|
||||
$device->ip, $mac, $port;
|
||||
$device_port->update({is_uplink => \'true'});
|
||||
|
||||
# when there's no CDP/LLDP, we only want to gather macs at the
|
||||
# topology edge, hence skip ports with known device macs.
|
||||
next unless setting('macsuck_bleed');
|
||||
}
|
||||
|
||||
++$cache->{$port}->{$mac};
|
||||
}
|
||||
|
||||
return $cache;
|
||||
}
|
||||
|
||||
=head2 store_wireless_client_info( $device, $snmp, $now? )
|
||||
|
||||
Given a Device database object, and a working SNMP connection, connect to a
|
||||
device and discover 802.11 related information for all connected wireless
|
||||
clients.
|
||||
|
||||
If the device doesn't support the 802.11 MIBs, then this will silently return.
|
||||
|
||||
If the device does support the 802.11 MIBs but Netdisco's configuration
|
||||
does not permit polling (C<store_wireless_client> must be true) then a debug
|
||||
message is logged and the subroutine returns.
|
||||
|
||||
Otherwise, client information is gathered and stored to the database.
|
||||
|
||||
Optionally, a third argument can be the literal string passed to the time_last
|
||||
field of the database record. If not provided, it defauls to C<now()>.
|
||||
|
||||
=cut
|
||||
|
||||
sub store_wireless_client_info {
|
||||
my ($device, $snmp, $now) = @_;
|
||||
$now ||= 'now()';
|
||||
|
||||
my $cd11_txrate = $snmp->cd11_txrate;
|
||||
return unless $cd11_txrate and scalar keys %$cd11_txrate;
|
||||
|
||||
if (setting('store_wireless_client')) {
|
||||
debug sprintf ' [%s] macsuck - gathering wireless client info',
|
||||
$device->ip;
|
||||
}
|
||||
else {
|
||||
debug sprintf ' [%s] macsuck - dot11 info available but skipped due to config',
|
||||
$device->ip;
|
||||
return;
|
||||
}
|
||||
|
||||
my $cd11_rateset = $snmp->cd11_rateset();
|
||||
my $cd11_uptime = $snmp->cd11_uptime();
|
||||
my $cd11_sigstrength = $snmp->cd11_sigstrength();
|
||||
my $cd11_sigqual = $snmp->cd11_sigqual();
|
||||
my $cd11_mac = $snmp->cd11_mac();
|
||||
my $cd11_port = $snmp->cd11_port();
|
||||
my $cd11_rxpkt = $snmp->cd11_rxpkt();
|
||||
my $cd11_txpkt = $snmp->cd11_txpkt();
|
||||
my $cd11_rxbyte = $snmp->cd11_rxbyte();
|
||||
my $cd11_txbyte = $snmp->cd11_txbyte();
|
||||
my $cd11_ssid = $snmp->cd11_ssid();
|
||||
|
||||
while (my ($idx, $txrates) = each %$cd11_txrate) {
|
||||
my $rates = $cd11_rateset->{$idx};
|
||||
my $mac = $cd11_mac->{$idx};
|
||||
next unless defined $mac; # avoid null entries
|
||||
# there can be more rows in txrate than other tables
|
||||
|
||||
my $txrate = defined $txrates->[$#$txrates]
|
||||
? int($txrates->[$#$txrates])
|
||||
: undef;
|
||||
|
||||
my $maxrate = defined $rates->[$#$rates]
|
||||
? int($rates->[$#$rates])
|
||||
: undef;
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
schema('netdisco')->resultset('NodeWireless')
|
||||
->search({ 'me.mac' => $mac })
|
||||
->update_or_create({
|
||||
txrate => $txrate,
|
||||
maxrate => $maxrate,
|
||||
uptime => $cd11_uptime->{$idx},
|
||||
rxpkt => $cd11_rxpkt->{$idx},
|
||||
txpkt => $cd11_txpkt->{$idx},
|
||||
rxbyte => $cd11_rxbyte->{$idx},
|
||||
txbyte => $cd11_txbyte->{$idx},
|
||||
sigqual => $cd11_sigqual->{$idx},
|
||||
sigstrength => $cd11_sigstrength->{$idx},
|
||||
ssid => ($cd11_ssid->{$idx} || 'unknown'),
|
||||
time_last => \$now,
|
||||
}, {
|
||||
order_by => [qw/mac ssid/],
|
||||
for => 'update',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -8,7 +8,7 @@ use base 'DBIx::Class::Schema';
|
||||
|
||||
__PACKAGE__->load_namespaces;
|
||||
|
||||
our $VERSION = 17; # schema version used for upgrades, keep as integer
|
||||
our $VERSION = 20; # schema version used for upgrades, keep as integer
|
||||
|
||||
use Path::Class;
|
||||
use File::Basename;
|
||||
@@ -17,7 +17,11 @@ my (undef, $libpath, undef) = fileparse( $INC{ 'App/Netdisco/DB.pm' } );
|
||||
our $schema_versions_dir = Path::Class::Dir->new($libpath)
|
||||
->subdir("DB", "schema_versions")->stringify;
|
||||
|
||||
__PACKAGE__->load_components(qw/Schema::Versioned/);
|
||||
__PACKAGE__->load_components(qw/
|
||||
Schema::Versioned
|
||||
+App::Netdisco::DB::ExplicitLocking
|
||||
/);
|
||||
|
||||
__PACKAGE__->upgrade_directory($schema_versions_dir);
|
||||
|
||||
1;
|
||||
|
||||
165
Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm
Normal file
165
Netdisco/lib/App/Netdisco/DB/ExplicitLocking.pm
Normal file
@@ -0,0 +1,165 @@
|
||||
package App::Netdisco::DB::ExplicitLocking;
|
||||
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
|
||||
our %lock_modes;
|
||||
|
||||
BEGIN {
|
||||
%lock_modes = (
|
||||
ACCESS_SHARE => 'ACCESS SHARE',
|
||||
ROW_SHARE => 'ROW SHARE',
|
||||
ROW_EXCLUSIVE => 'ROW EXCLUSIVE',
|
||||
SHARE_UPDATE_EXCLUSIVE => 'SHARE UPDATE EXCLUSIVE',
|
||||
SHARE => 'SHARE',
|
||||
SHARE_ROW_EXCLUSIVE => 'SHARE ROW EXCLUSIVE',
|
||||
EXCLUSIVE => 'EXCLUSIVE',
|
||||
ACCESS_EXCLUSIVE => 'ACCESS EXCLUSIVE',
|
||||
);
|
||||
}
|
||||
|
||||
use constant \%lock_modes;
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = (keys %lock_modes);
|
||||
our %EXPORT_TAGS = (modes => \@EXPORT_OK);
|
||||
|
||||
sub txn_do_locked {
|
||||
my ($self, $table, $mode, $sub) = @_;
|
||||
my $sql_fmt = q{LOCK TABLE %s IN %%s MODE};
|
||||
my $schema = $self;
|
||||
|
||||
if ($self->can('result_source')) {
|
||||
# ResultSet component
|
||||
$sub = $mode;
|
||||
$mode = $table;
|
||||
$table = $self->result_source->from;
|
||||
$schema = $self->result_source->schema;
|
||||
}
|
||||
|
||||
$schema->throw_exception('missing Table name to txn_do_locked()')
|
||||
unless length $table;
|
||||
|
||||
$table = [$table] if ref '' eq ref $table;
|
||||
my $table_fmt = join ', ', ('%s' x scalar @$table);
|
||||
my $sql = sprintf $sql_fmt, $table_fmt;
|
||||
|
||||
if (ref '' eq ref $mode and length $mode) {
|
||||
scalar grep {$_ eq $mode} values %lock_modes
|
||||
or $schema->throw_exception('bad LOCK_MODE to txn_do_locked()');
|
||||
}
|
||||
else {
|
||||
$sub = $mode;
|
||||
$mode = 'ACCESS EXCLUSIVE';
|
||||
}
|
||||
|
||||
$schema->txn_do(sub {
|
||||
my @params = map {$schema->storage->dbh->quote_identifier($_)} @$table;
|
||||
$schema->storage->dbh->do(sprintf $sql, @params, $mode);
|
||||
$sub->();
|
||||
});
|
||||
}
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::DB::ExplicitLocking - Support for PostgreSQL Lock Modes
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
In your L<DBIx::Class> schema:
|
||||
|
||||
package My::Schema;
|
||||
__PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');
|
||||
|
||||
Then, in your application code:
|
||||
|
||||
use App::Netdisco::DB::ExplicitLocking ':modes';
|
||||
$schema->txn_do_locked($table, MODE_NAME, sub { ... });
|
||||
|
||||
This also works for the ResultSet:
|
||||
|
||||
package My::Schema::ResultSet::TableName;
|
||||
__PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');
|
||||
|
||||
Then, in your application code:
|
||||
|
||||
use App::Netdisco::DB::ExplicitLocking ':modes';
|
||||
$schema->resultset('TableName')->txn_do_locked(MODE_NAME, sub { ... });
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This L<DBIx::Class> component provides an easy way to execute PostgreSQL table
|
||||
locks before a transaction block.
|
||||
|
||||
You can load the component in either the Schema class or ResultSet class (or
|
||||
both) and then use an interface very similar to C<DBIx::Class>'s C<txn_do()>.
|
||||
|
||||
The package also exports constants for each of the table lock modes supported
|
||||
by PostgreSQL, which must be used if specifying the mode (default mode is
|
||||
C<ACCESS EXCLUSIVE>).
|
||||
|
||||
=head1 EXPORTS
|
||||
|
||||
With the C<:modes> tag (as in SYNOPSIS above) the following constants are
|
||||
exported and must be used if specifying the lock mode:
|
||||
|
||||
=over 4
|
||||
|
||||
=item * C<ACCESS_SHARE>
|
||||
|
||||
=item * C<ROW_SHARE>
|
||||
|
||||
=item * C<ROW_EXCLUSIVE>
|
||||
|
||||
=item * C<SHARE_UPDATE_EXCLUSIVE>
|
||||
|
||||
=item * C<SHARE>
|
||||
|
||||
=item * C<SHARE_ROW_EXCLUSIVE>
|
||||
|
||||
=item * C<EXCLUSIVE>
|
||||
|
||||
=item * C<ACCESS_EXCLUSIVE>
|
||||
|
||||
=back
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 C<< $schema->txn_do_locked($table|\@tables, MODE_NAME?, $subref) >>
|
||||
|
||||
This is the method signature used when the component is loaded into your
|
||||
Schema class. The reason you might want to use this over the ResultSet version
|
||||
(below) is to specify multiple tables to be locked before the transaction.
|
||||
|
||||
The first argument is one or more tables, and is required. Note that these are
|
||||
the real table names in PostgreSQL, and not C<DBIx::Class> ResultSet aliases
|
||||
or anything like that.
|
||||
|
||||
The mode name is optional, and defaults to C<ACCESS EXCLUSIVE>. You must use
|
||||
one of the exported constants in this parameter.
|
||||
|
||||
Finally pass a subroutine reference, just as you would to the normal
|
||||
C<DBIx::Class> C<txn_do()> method. Note that additional arguments are not
|
||||
supported.
|
||||
|
||||
=head2 C<< $resultset->txn_do_locked(MODE_NAME?, $subref) >>
|
||||
|
||||
This is the method signature used when the component is loaded into your
|
||||
ResultSet class. If you don't yet have a ResultSet class (which is the default
|
||||
- normally only Result classes are created) then you can create a stub which
|
||||
simply loads this component (and inherits from C<DBIx::Class::ResultSet>).
|
||||
|
||||
This is the simplest way to use this module if you only want to lock one table
|
||||
before your transaction block.
|
||||
|
||||
The first argument is the optional mode name, which defaults to C<ACCESS
|
||||
EXCLUSIVE>. You must use one of the exported constants in this parameter.
|
||||
|
||||
The second argument is a subroutine reference, just as you would pass to the
|
||||
normal C<DBIx::Class> C<txn_do()> method. Note that additional arguments are
|
||||
not supported.
|
||||
|
||||
=cut
|
||||
|
||||
1;
|
||||
@@ -55,4 +55,46 @@ __PACKAGE__->add_columns(
|
||||
__PACKAGE__->set_primary_key("job");
|
||||
|
||||
# You can replace this text with custom code or comments, and it will be preserved on regeneration
|
||||
|
||||
=head1 ADDITIONAL COLUMNS
|
||||
|
||||
=head2 entererd_stamp
|
||||
|
||||
Formatted version of the C<entered> field, accurate to the minute.
|
||||
|
||||
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
|
||||
between the date stamp and time stamp. That is:
|
||||
|
||||
2012-02-06 12:49
|
||||
|
||||
=cut
|
||||
|
||||
sub entered_stamp { return (shift)->get_column('entered_stamp') }
|
||||
|
||||
=head2 started_stamp
|
||||
|
||||
Formatted version of the C<started> field, accurate to the minute.
|
||||
|
||||
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
|
||||
between the date stamp and time stamp. That is:
|
||||
|
||||
2012-02-06 12:49
|
||||
|
||||
=cut
|
||||
|
||||
sub started_stamp { return (shift)->get_column('started_stamp') }
|
||||
|
||||
=head2 finished_stamp
|
||||
|
||||
Formatted version of the C<finished> field, accurate to the minute.
|
||||
|
||||
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
|
||||
between the date stamp and time stamp. That is:
|
||||
|
||||
2012-02-06 12:49
|
||||
|
||||
=cut
|
||||
|
||||
sub finished_stamp { return (shift)->get_column('finished_stamp') }
|
||||
|
||||
1;
|
||||
|
||||
@@ -180,6 +180,16 @@ __PACKAGE__->has_many(
|
||||
|
||||
=head1 ADDITIONAL COLUMNS
|
||||
|
||||
=head2 port_count
|
||||
|
||||
Returns the number of ports on this device. Enable this
|
||||
column by applying the C<with_port_count()> modifier to C<search()>.
|
||||
|
||||
=cut
|
||||
|
||||
sub port_count { return (shift)->get_column('port_count') }
|
||||
|
||||
|
||||
=head2 uptime_age
|
||||
|
||||
Formatted version of the C<uptime> field.
|
||||
|
||||
@@ -7,6 +7,8 @@ package App::Netdisco::DB::Result::DevicePort;
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use MIME::Base64 'encode_base64url';
|
||||
|
||||
use base 'DBIx::Class::Core';
|
||||
__PACKAGE__->table("device_port");
|
||||
__PACKAGE__->add_columns(
|
||||
@@ -51,6 +53,10 @@ __PACKAGE__->add_columns(
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"remote_id",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"manual_topo",
|
||||
{ data_type => "bool", is_nullable => 0, default_value => \"false" },
|
||||
"is_uplink",
|
||||
{ data_type => "bool", is_nullable => 1 },
|
||||
"vlan",
|
||||
{ data_type => "text", is_nullable => 1 },
|
||||
"pvid",
|
||||
@@ -262,4 +268,13 @@ See the C<with_is_free> and C<only_free_ports> modifiers to C<search()>.
|
||||
|
||||
sub is_free { return (shift)->get_column('is_free') }
|
||||
|
||||
=head2 base64url_port
|
||||
|
||||
Returns a Base64 encoded version of the C<port> column value suitable for use
|
||||
in a URL.
|
||||
|
||||
=cut
|
||||
|
||||
sub base64url_port { return encode_base64url((shift)->port) }
|
||||
|
||||
1;
|
||||
|
||||
@@ -44,7 +44,7 @@ __PACKAGE__->add_columns(
|
||||
original => { default_value => \"now()" },
|
||||
},
|
||||
"vlan",
|
||||
{ data_type => "text", is_nullable => 1, default_value => '0' },
|
||||
{ data_type => "text", is_nullable => 0, default_value => '0' },
|
||||
);
|
||||
__PACKAGE__->set_primary_key("mac", "switch", "port", "vlan");
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ __PACKAGE__->add_columns(
|
||||
original => { default_value => \"now()" },
|
||||
},
|
||||
"ssid",
|
||||
{ data_type => "text", is_nullable => 1, default_value => '' },
|
||||
{ data_type => "text", is_nullable => 0, default_value => '' },
|
||||
);
|
||||
__PACKAGE__->set_primary_key("mac", "ssid");
|
||||
|
||||
|
||||
@@ -19,4 +19,7 @@ __PACKAGE__->add_columns(
|
||||
{ data_type => "text", is_nullable => 0 },
|
||||
);
|
||||
|
||||
__PACKAGE__->add_unique_constraint(['dev1','port1']);
|
||||
__PACKAGE__->add_unique_constraint(['dev2','port2']);
|
||||
|
||||
1;
|
||||
|
||||
41
Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm
Normal file
41
Netdisco/lib/App/Netdisco/DB/ResultSet/Admin.pm
Normal file
@@ -0,0 +1,41 @@
|
||||
package App::Netdisco::DB::ResultSet::Admin;
|
||||
use base 'DBIx::Class::ResultSet';
|
||||
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
|
||||
=head1 ADDITIONAL METHODS
|
||||
|
||||
=head2 with_times
|
||||
|
||||
This is a modifier for any C<search()> (including the helpers below) which
|
||||
will add the following additional synthesized columns to the result set:
|
||||
|
||||
=over 4
|
||||
|
||||
=item entered_stamp
|
||||
|
||||
=item started_stamp
|
||||
|
||||
=item finished_stamp
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub with_times {
|
||||
my ($rs, $cond, $attrs) = @_;
|
||||
|
||||
return $rs
|
||||
->search_rs($cond, $attrs)
|
||||
->search({},
|
||||
{
|
||||
'+columns' => {
|
||||
entered_stamp => \"to_char(entered, 'YYYY-MM-DD HH24:MI')",
|
||||
started_stamp => \"to_char(started, 'YYYY-MM-DD HH24:MI')",
|
||||
finished_stamp => \"to_char(finished, 'YYYY-MM-DD HH24:MI')",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -491,4 +491,34 @@ sub get_distinct_col {
|
||||
)->get_column($col)->all;
|
||||
}
|
||||
|
||||
=head2 with_port_count
|
||||
|
||||
This is a modifier for any C<search()> which
|
||||
will add the following additional synthesized column to the result set:
|
||||
|
||||
=over 4
|
||||
|
||||
=item port_count
|
||||
|
||||
=back
|
||||
|
||||
=cut
|
||||
|
||||
sub with_port_count {
|
||||
my ($rs, $cond, $attrs) = @_;
|
||||
|
||||
return $rs
|
||||
->search_rs($cond, $attrs)
|
||||
->search({},
|
||||
{
|
||||
'+columns' => { port_count =>
|
||||
$rs->result_source->schema->resultset('DevicePort')
|
||||
->search(
|
||||
{ 'dp.ip' => { -ident => 'me.ip' } },
|
||||
{ alias => 'dp' }
|
||||
)->count_rs->as_query
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
@@ -4,6 +4,10 @@ use base 'DBIx::Class::ResultSet';
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
|
||||
__PACKAGE__->load_components(qw/
|
||||
+App::Netdisco::DB::ExplicitLocking
|
||||
/);
|
||||
|
||||
=head1 search_by_mac( \%cond, \%attrs? )
|
||||
|
||||
my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55', active => 1});
|
||||
|
||||
@@ -4,6 +4,10 @@ use base 'DBIx::Class::ResultSet';
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
|
||||
__PACKAGE__->load_components(qw/
|
||||
+App::Netdisco::DB::ExplicitLocking
|
||||
/);
|
||||
|
||||
my $search_attr = {
|
||||
order_by => {'-desc' => 'time_last'},
|
||||
'+columns' => [
|
||||
|
||||
11
Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm
Normal file
11
Netdisco/lib/App/Netdisco/DB/ResultSet/NodeWireless.pm
Normal file
@@ -0,0 +1,11 @@
|
||||
package App::Netdisco::DB::ResultSet::NodeWireless;
|
||||
use base 'DBIx::Class::ResultSet';
|
||||
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
|
||||
__PACKAGE__->load_components(qw/
|
||||
+App::Netdisco::DB::ExplicitLocking
|
||||
/);
|
||||
|
||||
1;
|
||||
11
Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm
Normal file
11
Netdisco/lib/App/Netdisco/DB/ResultSet/Subnet.pm
Normal file
@@ -0,0 +1,11 @@
|
||||
package App::Netdisco::DB::ResultSet::Subnet;
|
||||
use base 'DBIx::Class::ResultSet';
|
||||
|
||||
use strict;
|
||||
use warnings FATAL => 'all';
|
||||
|
||||
__PACKAGE__->load_components(qw/
|
||||
+App::Netdisco::DB::ExplicitLocking
|
||||
/);
|
||||
|
||||
1;
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Convert schema '/home/devver/netdisco-ng/Netdisco/bin/../lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-17-PostgreSQL.sql' to '/home/devver/netdisco-ng/Netdisco/bin/../lib/App/Netdisco/DB/schema_versions/App-Netdisco-DB-18-PostgreSQL.sql':;
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE topology ADD CONSTRAINT topology_dev1_port1 UNIQUE (dev1, port1);
|
||||
|
||||
ALTER TABLE topology ADD CONSTRAINT topology_dev2_port2 UNIQUE (dev2, port2);
|
||||
|
||||
COMMIT;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE device_port ADD COLUMN "manual_topo" bool DEFAULT false NOT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,6 @@
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE device_port ADD COLUMN "is_uplink" bool;
|
||||
ALTER TABLE device_port ADD COLUMN "is_uplink_admin" bool;
|
||||
|
||||
COMMIT;
|
||||
@@ -33,7 +33,7 @@ sub capacity_for {
|
||||
debug "checking local capacity for action $action";
|
||||
|
||||
my $action_map = {
|
||||
Poller => [qw/refresh discover discovernew discover_neighbors/],
|
||||
Poller => [qw/discoverall discover arpwalk arpnip macwalk macsuck/],
|
||||
Interactive => [qw/location contact portcontrol portname vlan power/],
|
||||
};
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ sub close_job {
|
||||
|
||||
try {
|
||||
schema('netdisco')->resultset('Admin')
|
||||
->find($job->job)
|
||||
->find($job->job, {for => 'update'})
|
||||
->update({
|
||||
status => $status,
|
||||
log => $log,
|
||||
|
||||
@@ -14,7 +14,7 @@ my $fqdn = hostfqdn || 'localhost';
|
||||
|
||||
my $role_map = {
|
||||
(map {$_ => 'Poller'}
|
||||
qw/refresh discover discovernew discover_neighbors/),
|
||||
qw/discoverall discover arpwalk arpnip macwalk macsuck/),
|
||||
(map {$_ => 'Interactive'}
|
||||
qw/location contact portcontrol portname vlan power/)
|
||||
};
|
||||
|
||||
@@ -10,6 +10,8 @@ use namespace::clean;
|
||||
|
||||
# add dispatch methods for poller tasks
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Device';
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Arpnip';
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Macsuck';
|
||||
|
||||
sub worker_body {
|
||||
my $self = shift;
|
||||
@@ -61,7 +63,7 @@ sub close_job {
|
||||
|
||||
try {
|
||||
schema('netdisco')->resultset('Admin')
|
||||
->find($job->job)
|
||||
->find($job->job, {for => 'update'})
|
||||
->update({
|
||||
status => $status,
|
||||
log => $log,
|
||||
|
||||
71
Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm
Normal file
71
Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Arpnip.pm
Normal file
@@ -0,0 +1,71 @@
|
||||
package App::Netdisco::Daemon::Worker::Poller::Arpnip;
|
||||
|
||||
use Dancer qw/:moose :syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::SNMP 'snmp_connect';
|
||||
use App::Netdisco::Util::Device 'get_device';
|
||||
use App::Netdisco::Core::Arpnip 'do_arpnip';
|
||||
use App::Netdisco::Daemon::Util ':all';
|
||||
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
|
||||
use Role::Tiny;
|
||||
use namespace::clean;
|
||||
|
||||
# queue an arpnip job for all devices known to Netdisco
|
||||
sub arpwalk {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $devices = schema('netdisco')->resultset('Device')->get_column('ip');
|
||||
my $jobqueue = schema('netdisco')->resultset('Admin');
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# clean up user submitted jobs older than 1min,
|
||||
# assuming skew between schedulers' clocks is not greater than 1min
|
||||
$jobqueue->search({
|
||||
action => 'arpnip',
|
||||
status => 'queued',
|
||||
entered => { '<' => \"(now() - interval '1 minute')" },
|
||||
})->delete;
|
||||
|
||||
# is scuppered by any user job submitted in last 1min (bad), or
|
||||
# any similar job from another scheduler (good)
|
||||
$jobqueue->populate([
|
||||
map {{
|
||||
device => $_,
|
||||
action => 'arpnip',
|
||||
status => 'queued',
|
||||
}} ($devices->all)
|
||||
]);
|
||||
});
|
||||
|
||||
return job_done("Queued arpnip job for all devices");
|
||||
}
|
||||
|
||||
sub arpnip {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $host = NetAddr::IP::Lite->new($job->device);
|
||||
my $device = get_device($host->addr);
|
||||
|
||||
if ($device->in_storage
|
||||
and $device->vendor and $device->vendor eq 'netdisco') {
|
||||
return job_done("Skipped arpnip for pseudo-device $host");
|
||||
}
|
||||
|
||||
my $snmp = snmp_connect($device);
|
||||
if (!defined $snmp) {
|
||||
return job_error("arpnip failed: could not SNMP connect to $host");
|
||||
}
|
||||
|
||||
unless ($snmp->has_layer(3)) {
|
||||
return job_done("Skipped arpnip for device $host without OSI layer 3 capability");
|
||||
}
|
||||
|
||||
do_arpnip($device, $snmp);
|
||||
|
||||
return job_done("Ended arpnip for ". $host->addr);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -5,7 +5,7 @@ use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::SNMP 'snmp_connect';
|
||||
use App::Netdisco::Util::Device 'get_device';
|
||||
use App::Netdisco::Util::DiscoverAndStore ':all';
|
||||
use App::Netdisco::Core::Discover ':all';
|
||||
use App::Netdisco::Daemon::Util ':all';
|
||||
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
@@ -14,29 +14,48 @@ use Role::Tiny;
|
||||
use namespace::clean;
|
||||
|
||||
# queue a discover job for all devices known to Netdisco
|
||||
sub refresh {
|
||||
sub discoverall {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $devices = schema('netdisco')->resultset('Device')->get_column('ip');
|
||||
my $jobqueue = schema('netdisco')->resultset('Admin');
|
||||
|
||||
schema('netdisco')->resultset('Admin')->populate([
|
||||
map {{
|
||||
device => $_,
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# clean up user submitted jobs older than 1min,
|
||||
# assuming skew between schedulers' clocks is not greater than 1min
|
||||
$jobqueue->search({
|
||||
action => 'discover',
|
||||
status => 'queued',
|
||||
}} ($devices->all)
|
||||
]);
|
||||
entered => { '<' => \"(now() - interval '1 minute')" },
|
||||
})->delete;
|
||||
|
||||
# is scuppered by any user job submitted in last 1min (bad), or
|
||||
# any similar job from another scheduler (good)
|
||||
$jobqueue->populate([
|
||||
map {{
|
||||
device => $_,
|
||||
action => 'discover',
|
||||
status => 'queued',
|
||||
}} ($devices->all)
|
||||
]);
|
||||
});
|
||||
|
||||
return job_done("Queued discover job for all devices");
|
||||
}
|
||||
|
||||
# queue a discover job for one device, and its *new* neighbors
|
||||
sub discover {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $host = NetAddr::IP::Lite->new($job->device);
|
||||
my $device = get_device($host->addr);
|
||||
my $snmp = snmp_connect($device);
|
||||
|
||||
if ($device->in_storage
|
||||
and $device->vendor and $device->vendor eq 'netdisco') {
|
||||
return job_done("Skipped discover for pseudo-device $host");
|
||||
}
|
||||
|
||||
my $snmp = snmp_connect($device);
|
||||
if (!defined $snmp) {
|
||||
return job_error("discover failed: could not SNMP connect to $host");
|
||||
}
|
||||
@@ -47,42 +66,9 @@ sub discover {
|
||||
store_vlans($device, $snmp);
|
||||
store_power($device, $snmp);
|
||||
store_modules($device, $snmp);
|
||||
discover_new_neighbors($device, $snmp);
|
||||
|
||||
return job_done("Ended discover for $host");
|
||||
}
|
||||
|
||||
# run find_neighbors on all known devices, and run discover on any
|
||||
# newly found devices.
|
||||
sub discovernew {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $devices = schema('netdisco')->resultset('Device')->get_column('ip');
|
||||
|
||||
schema('netdisco')->resultset('Admin')->populate([
|
||||
map {{
|
||||
device => $_,
|
||||
action => 'discover_neighbors',
|
||||
status => 'queued',
|
||||
}} ($devices->all)
|
||||
]);
|
||||
|
||||
return job_done("Queued discover_neighbors job for all devices");
|
||||
}
|
||||
|
||||
sub discover_neighbors {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $host = NetAddr::IP::Lite->new($job->device);
|
||||
my $device = get_device($host->addr);
|
||||
my $snmp = snmp_connect($device);
|
||||
|
||||
if (!defined $snmp) {
|
||||
return job_error("discover_neighbors failed: could not SNMP connect to $host");
|
||||
}
|
||||
|
||||
find_neighbors($device, $snmp);
|
||||
|
||||
return job_done("Ended find_neighbors for $host");
|
||||
return job_done("Ended discover for ". $host->addr);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
71
Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Macsuck.pm
Normal file
71
Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Macsuck.pm
Normal file
@@ -0,0 +1,71 @@
|
||||
package App::Netdisco::Daemon::Worker::Poller::Macsuck;
|
||||
|
||||
use Dancer qw/:moose :syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::SNMP 'snmp_connect';
|
||||
use App::Netdisco::Util::Device 'get_device';
|
||||
use App::Netdisco::Core::Macsuck ':all';
|
||||
use App::Netdisco::Daemon::Util ':all';
|
||||
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
|
||||
use Role::Tiny;
|
||||
use namespace::clean;
|
||||
|
||||
# queue a macsuck job for all devices known to Netdisco
|
||||
sub macwalk {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $devices = schema('netdisco')->resultset('Device')->get_column('ip');
|
||||
my $jobqueue = schema('netdisco')->resultset('Admin');
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# clean up user submitted jobs older than 1min,
|
||||
# assuming skew between schedulers' clocks is not greater than 1min
|
||||
$jobqueue->search({
|
||||
action => 'macsuck',
|
||||
status => 'queued',
|
||||
entered => { '<' => \"(now() - interval '1 minute')" },
|
||||
})->delete;
|
||||
|
||||
# is scuppered by any user job submitted in last 1min (bad), or
|
||||
# any similar job from another scheduler (good)
|
||||
$jobqueue->populate([
|
||||
map {{
|
||||
device => $_,
|
||||
action => 'macsuck',
|
||||
status => 'queued',
|
||||
}} ($devices->all)
|
||||
]);
|
||||
});
|
||||
|
||||
return job_done("Queued macsuck job for all devices");
|
||||
}
|
||||
|
||||
sub macsuck {
|
||||
my ($self, $job) = @_;
|
||||
|
||||
my $host = NetAddr::IP::Lite->new($job->device);
|
||||
my $device = get_device($host->addr);
|
||||
|
||||
if ($device->in_storage
|
||||
and $device->vendor and $device->vendor eq 'netdisco') {
|
||||
return job_done("Skipped macsuck for pseudo-device $host");
|
||||
}
|
||||
|
||||
my $snmp = snmp_connect($device);
|
||||
if (!defined $snmp) {
|
||||
return job_error("macsuck failed: could not SNMP connect to $host");
|
||||
}
|
||||
|
||||
unless ($snmp->has_layer(2)) {
|
||||
return job_done("Skipped macsuck for device $host without OSI layer 2 capability");
|
||||
}
|
||||
|
||||
do_macsuck($device, $snmp);
|
||||
|
||||
return job_done("Ended macsuck for ". $host->addr);
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -11,12 +11,11 @@ use namespace::clean;
|
||||
|
||||
my $jobactions = {
|
||||
map {$_ => undef} qw/
|
||||
refresh
|
||||
discovernew
|
||||
discoverall
|
||||
arpwalk
|
||||
macwalk
|
||||
/
|
||||
# saveconfigs
|
||||
# macwalk
|
||||
# arpwalk
|
||||
# nbtwalk
|
||||
# backup
|
||||
};
|
||||
|
||||
142
Netdisco/lib/App/Netdisco/Manual/Configuration.pod
Normal file
142
Netdisco/lib/App/Netdisco/Manual/Configuration.pod
Normal file
@@ -0,0 +1,142 @@
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Manual::Configuration - How to Configure Netdisco
|
||||
|
||||
=head1 INTRODUCTION
|
||||
|
||||
The configuration files for Netdisco come with all options set to sensible
|
||||
default values, and just a few that you must initially set yourself.
|
||||
|
||||
However as you use the system over time, there are many situations where you
|
||||
might want to tune the behaviour of Netdisco, and for that we have a lot of
|
||||
configuration settings available.
|
||||
|
||||
=head2 GUIDANCE
|
||||
|
||||
There are two configuration files: C<config.yml> (which lives inside Netdisco)
|
||||
and C<deployment.yml> (which usually lives in C<${HOME}/environments>).
|
||||
|
||||
The C<config.yml> file includes defaults for every setting, and should be left
|
||||
alone. Any time you want to set an option, use only the C<deployment.yml>
|
||||
file. The two are merged when Netdisco starts, with your settings in
|
||||
C<deployment.yml> overriding the defaults from C<config.yml>.
|
||||
|
||||
The configuration file format for Netdisco is YAML. This is easy for humans to
|
||||
edit, but you should take care over whitespace and avoid TAB characters. YAML
|
||||
supports several data types:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
Boolean - True/False value, using C<1> and C<0> or C<true> and C<false>
|
||||
respectively
|
||||
|
||||
=item *
|
||||
|
||||
List - Set of things using C<[a, b, c]> on one line or C<-> on separate lines
|
||||
|
||||
=item *
|
||||
|
||||
Dictionary - Key/Value pairs (like Perl Hash) using C<{key1: val1, key2,
|
||||
val2}> on one line or C<key: value> on separate lines
|
||||
|
||||
=item *
|
||||
|
||||
String - Quoted, just like in Perl (and essential if the item contains the
|
||||
colon character)
|
||||
|
||||
=back
|
||||
|
||||
=head1 SUPPORTED SETTINGS
|
||||
|
||||
=head2 Essential Settings
|
||||
|
||||
If you followed the installation instructions, then you should have set the
|
||||
database connection parameters to match those of your local system. That is,
|
||||
the C<dsn> (DB name, host, port), C<user> and C<pass>.
|
||||
|
||||
=head2 General Settings
|
||||
|
||||
=head3 C<log: debug|warning|error>
|
||||
|
||||
Default: C<warning>
|
||||
|
||||
The log level used by Netdisco. It's useful to see warning messages from the
|
||||
backend poller, as this can highlight broken topology.
|
||||
|
||||
=head3 C<logger: console|file>
|
||||
|
||||
Default: C<file>
|
||||
|
||||
Destination for log messages. Console means standard ouput. When set to
|
||||
C<file>, the default destination is the C<${HOME}/logs> directory.
|
||||
|
||||
=head3 C<logger_format: String>
|
||||
|
||||
Default: C<< '[%P] %L @%D> %m' >>
|
||||
|
||||
Structure of the log messages. See L<Dancer::Logger::Abstract/"logger_format">
|
||||
for details.
|
||||
|
||||
=head2 Web Frontend
|
||||
|
||||
=head3 C<domain_suffix: String>
|
||||
|
||||
Default: None
|
||||
|
||||
Set this to your local site's domain name. This is usually removed from node
|
||||
names in the web interface to make things more readable.
|
||||
|
||||
=head3 C<no_auth: Boolean>
|
||||
|
||||
Default: C<false>
|
||||
|
||||
Enable this to disable login authentication in the web frontend. The username
|
||||
will be set to C<guest> so if you want to allow extended permissions (C<admin>
|
||||
or C<port_control>, create a dummy user with the appropriate flag, in the
|
||||
database:
|
||||
|
||||
netdisco=> insert into users (username, port_control) values ('guest', true);
|
||||
|
||||
=head3 C<port: String>
|
||||
|
||||
Default: C<5000>
|
||||
|
||||
Port which the web server listens on. Netdisco comes with a good pre-forking
|
||||
web server, so you can change this to C<80> if you want to use it directly.
|
||||
However the default is designed to work well with servers such as Apache in
|
||||
reverse-proxy mode.
|
||||
|
||||
=head3 C<web_plugins: List of String>
|
||||
|
||||
Default: List of L<App::Netdisco::Web::Plugin> names
|
||||
|
||||
Netdisco's plugin system allows the user more control over the user interface.
|
||||
Plugins can be distributed independently from Netdisco and are a better
|
||||
alternative to source code patches. This setting is the list of Plugins which
|
||||
are used in the default Netdisco distribution.
|
||||
|
||||
You can override this to set your own list. If you only want to add to the
|
||||
default list then use C<extra_web_plugins>, which allows the Netdisco
|
||||
developers to update C<web_plugins> in a future release.
|
||||
|
||||
=head3 C<extra_web_plugins: List of String>
|
||||
|
||||
Default: None
|
||||
|
||||
List of additional L<App::Netdisco::Web::Plugin> names to load. See also the
|
||||
C<web_plugins> setting.
|
||||
|
||||
=head2 Netdisco Core
|
||||
|
||||
=head2 Backend Daemon
|
||||
|
||||
=head2 Dancer Internal
|
||||
|
||||
=head1 UNSUPPORTED SETTINGS
|
||||
|
||||
These settings are from Netdisco 1.x but are yet to be supported in Netdisco
|
||||
2. If you really need the feature, please let the developers know.
|
||||
|
||||
=cut
|
||||
@@ -22,6 +22,11 @@ parameter to the web startup script:
|
||||
|
||||
~/bin/netdisco-web --path /netdisco2
|
||||
|
||||
Alternatively, can set the C<path> configuration option in your
|
||||
C<deployment.yml> file:
|
||||
|
||||
path: '/netdisco2'
|
||||
|
||||
=head1 Behind a Proxy
|
||||
|
||||
By default the web application daemon starts listening on port 5000 and goes
|
||||
@@ -39,8 +44,13 @@ configuration would be:
|
||||
Allow from all
|
||||
</Proxy>
|
||||
|
||||
You also need to set the following configuration in your C<deployment.yml>
|
||||
file:
|
||||
|
||||
behind_proxy: 1
|
||||
|
||||
To combine this with Non-root Hosting as above, simply change the paths
|
||||
referenced in the configuration like so (and use C<--path> option):
|
||||
referenced in the configuration like so (and use Non-root Hosting as above):
|
||||
|
||||
ProxyPass /netdisco2 http://localhost:5000/
|
||||
ProxyPassReverse /netdisco2 http://localhost:5000/
|
||||
|
||||
@@ -175,6 +175,24 @@ any query parameters which might customize the report search.
|
||||
See the L<App::Netdisco::Web::Plugin::Report::DuplexMismatch> module for a
|
||||
simple example of how to implement the handler.
|
||||
|
||||
=head1 Admin Tasks
|
||||
|
||||
These components appear in the black navigation bar under an Admin menu, but only
|
||||
if the logged in user has Administrator rights in Netdisco.
|
||||
|
||||
To register an item for display in the Admin menu, use the following code:
|
||||
|
||||
register_admin_task({
|
||||
tag => 'newfeature',
|
||||
label => 'My New Feature',
|
||||
});
|
||||
|
||||
This causes an item to appear in the Admin menu with a visible text of "My New
|
||||
Feature" which when clicked sends the user to the C</admin/mynewfeature> page.
|
||||
Note that this won't work for any target link - the path must be an
|
||||
App::Netdisco Dancer route handler. Please bug the App::Netdisco devs if you
|
||||
want arbitrary links supported.
|
||||
|
||||
=head1 Templates
|
||||
|
||||
All of Netdisco's web page templates are stashed away in its distribution,
|
||||
|
||||
56
Netdisco/lib/App/Netdisco/Util/PortMAC.pm
Normal file
56
Netdisco/lib/App/Netdisco/Util/PortMAC.pm
Normal file
@@ -0,0 +1,56 @@
|
||||
package App::Netdisco::Util::PortMAC;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/ get_port_macs /;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Util::PortMAC
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Helper subroutine to support parts of the Netdisco application.
|
||||
|
||||
There are no default exports, however the C<:all> tag will export all
|
||||
subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 get_port_macs( $device )
|
||||
|
||||
Returns a Hash reference of C<< { MAC => IP } >> for all interface MAC
|
||||
addresses on a device.
|
||||
|
||||
=cut
|
||||
|
||||
sub get_port_macs {
|
||||
my $device = shift;
|
||||
my $port_macs = {};
|
||||
|
||||
unless ($device->in_storage) {
|
||||
debug sprintf ' [%s] get_port_macs - skipping device not yet discovered',
|
||||
$device->ip;
|
||||
return $port_macs;
|
||||
}
|
||||
|
||||
my $dp_macs = schema('netdisco')->resultset('DevicePort')
|
||||
->search({ mac => { '!=' => undef} });
|
||||
while (my $r = $dp_macs->next) {
|
||||
$port_macs->{ $r->mac } = $r->ip;
|
||||
}
|
||||
|
||||
my $d_macs = schema('netdisco')->resultset('Device')
|
||||
->search({ mac => { '!=' => undef} });
|
||||
while (my $r = $d_macs->next) {
|
||||
$port_macs->{ $r->mac } = $r->ip;
|
||||
}
|
||||
|
||||
return $port_macs;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -10,7 +10,7 @@ use Path::Class 'dir';
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/
|
||||
snmp_connect snmp_connect_rw
|
||||
snmp_connect snmp_connect_rw snmp_comm_reindex
|
||||
/;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
@@ -88,8 +88,8 @@ sub _snmp_connect_generic {
|
||||
my $comm_type = pop;
|
||||
my @communities = @{ setting($comm_type) || []};
|
||||
unshift @communities, $device->snmp_comm
|
||||
if length $device->snmp_comm
|
||||
and length $comm_type and $comm_type eq 'community';
|
||||
if defined $device->snmp_comm
|
||||
and defined $comm_type and $comm_type eq 'community';
|
||||
|
||||
my $info = undef;
|
||||
VERSION: foreach my $ver (@versions) {
|
||||
@@ -123,7 +123,7 @@ sub _try_connect {
|
||||
$info = $class->new(%$snmp_args, Version => $ver, Community => $comm);
|
||||
undef $info unless (
|
||||
(not defined $info->error)
|
||||
and length $info->uptime
|
||||
and defined $info->uptime
|
||||
and ($info->layers or $info->description)
|
||||
and $info->class
|
||||
);
|
||||
@@ -149,7 +149,35 @@ sub _try_connect {
|
||||
sub _build_mibdirs {
|
||||
my $home = (setting('mibhome') || $ENV{NETDISCO_HOME} || $ENV{HOME});
|
||||
return map { dir($home, $_) }
|
||||
@{ setting('mibdirs') || [] };
|
||||
@{ setting('mibdirs') || _get_mibdirs_content($home) };
|
||||
}
|
||||
|
||||
sub _get_mibdirs_content {
|
||||
my $home = shift;
|
||||
warning 'Netdisco SNMP work will be really slow - loading ALL MIBs. Please set mibdirs.';
|
||||
my @list = map {s|$home/||; $_} grep {-d} glob("$home/*");
|
||||
return \@list;
|
||||
}
|
||||
|
||||
=head2 snmp_comm_reindex( $snmp, $vlan )
|
||||
|
||||
Takes an established L<SNMP::Info> instance and makes a fresh connection using
|
||||
community indexing, with the given C<$vlan> ID. Works for all SNMP versions.
|
||||
|
||||
=cut
|
||||
|
||||
sub snmp_comm_reindex {
|
||||
my ($snmp, $vlan) = @_;
|
||||
|
||||
my $ver = $snmp->snmp_ver;
|
||||
my $comm = $snmp->snmp_comm;
|
||||
|
||||
if ($ver == 3) {
|
||||
$snmp->update(Context => "vlan-$vlan");
|
||||
}
|
||||
else {
|
||||
$snmp->update(Community => $comm . '@' . $vlan);
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
115
Netdisco/lib/App/Netdisco/Util/SanityCheck.pm
Normal file
115
Netdisco/lib/App/Netdisco/Util/SanityCheck.pm
Normal file
@@ -0,0 +1,115 @@
|
||||
package App::Netdisco::Util::SanityCheck;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::PortMAC ':all';
|
||||
use Net::MAC;
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/ check_mac /;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Util::SanityCheck
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Helper subroutines to support parts of the Netdisco application.
|
||||
|
||||
There are no default exports, however the C<:all> tag will export all
|
||||
subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 check_mac( $device, $node, $port_macs? )
|
||||
|
||||
Given a Device database object and a MAC address, perform various sanity
|
||||
checks which need to be done before writing an ARP/Neighbor entry to the
|
||||
database storage.
|
||||
|
||||
Returns false, and might log a debug level message, if the checks fail.
|
||||
|
||||
Returns a true value if these checks pass:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
MAC address is well-formed (according to common formats)
|
||||
|
||||
=item *
|
||||
|
||||
MAC address is not all-zero, broadcast, CLIP, VRRP or HSRP
|
||||
|
||||
=item *
|
||||
|
||||
MAC address does not belong to an interface on any known Device
|
||||
|
||||
=back
|
||||
|
||||
Optionally pass a cached set of Device port MAC addresses as the third
|
||||
argument, or else C<check_mac> will retrieve this for itself from the
|
||||
database.
|
||||
|
||||
=cut
|
||||
|
||||
sub check_mac {
|
||||
my ($device, $node, $port_macs) = @_;
|
||||
$port_macs ||= get_port_macs($device);
|
||||
my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0);
|
||||
|
||||
# incomplete MAC addresses (BayRS frame relay DLCI, etc)
|
||||
if ($mac->get_error) {
|
||||
debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping',
|
||||
$device->ip, $node;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
# lower case, hex, colon delimited, 8-bit groups
|
||||
$node = lc $mac->as_IEEE;
|
||||
}
|
||||
|
||||
# broadcast MAC addresses
|
||||
return 0 if $node eq 'ff:ff:ff:ff:ff:ff';
|
||||
|
||||
# all-zero MAC addresses
|
||||
return 0 if $node eq '00:00:00:00:00:00';
|
||||
|
||||
# CLIP
|
||||
return 0 if $node eq '00:00:00:00:00:01';
|
||||
|
||||
# multicast
|
||||
if ($node =~ m/^[0-9a-f](?:1|3|5|7|9|b|d|f):/) {
|
||||
debug sprintf ' [%s] check_mac - multicast mac [%s] - skipping',
|
||||
$device->ip, $node;
|
||||
return 0;
|
||||
}
|
||||
|
||||
# VRRP
|
||||
if (index($node, '00:00:5e:00:01:') == 0) {
|
||||
debug sprintf ' [%s] check_mac - VRRP mac [%s] - skipping',
|
||||
$device->ip, $node;
|
||||
return 0;
|
||||
}
|
||||
|
||||
# HSRP
|
||||
if (index($node, '00:00:0c:07:ac:') == 0) {
|
||||
debug sprintf ' [%s] check_mac - HSRP mac [%s] - skipping',
|
||||
$device->ip, $node;
|
||||
return 0;
|
||||
}
|
||||
|
||||
# device's own MACs
|
||||
if (exists $port_macs->{$node}) {
|
||||
debug sprintf ' [%s] check_mac - mac [%s] is device port - skipping',
|
||||
$device->ip, $node;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -8,11 +8,14 @@ use Dancer::Plugin::DBIC;
|
||||
use Socket6 (); # to ensure dependency is met
|
||||
use HTML::Entities (); # to ensure dependency is met
|
||||
use URI::QueryParam (); # part of URI, to add helper methods
|
||||
use Path::Class 'dir';
|
||||
|
||||
use App::Netdisco::Web::AuthN;
|
||||
use App::Netdisco::Web::Static;
|
||||
use App::Netdisco::Web::Search;
|
||||
use App::Netdisco::Web::Device;
|
||||
use App::Netdisco::Web::Report;
|
||||
use App::Netdisco::Web::AdminTask;
|
||||
use App::Netdisco::Web::TypeAhead;
|
||||
use App::Netdisco::Web::PortControl;
|
||||
|
||||
@@ -20,8 +23,9 @@ sub _load_web_plugins {
|
||||
my $plugin_list = shift;
|
||||
|
||||
foreach my $plugin (@$plugin_list) {
|
||||
$plugin =~ s/^X::/+App::NetdiscoX::Web::Plugin::/;
|
||||
$plugin = 'App::Netdisco::Web::Plugin::'. $plugin
|
||||
unless $plugin =~ m/^\+/;
|
||||
if $plugin !~ m/^\+/;
|
||||
$plugin =~ s/^\+//;
|
||||
|
||||
debug "loading Netdisco plugin $plugin";
|
||||
@@ -35,6 +39,7 @@ if (setting('web_plugins') and ref [] eq ref setting('web_plugins')) {
|
||||
}
|
||||
|
||||
if (setting('extra_web_plugins') and ref [] eq ref setting('extra_web_plugins')) {
|
||||
unshift @INC, dir(($ENV{NETDISCO_HOME} || $ENV{HOME}), 'site_plugins')->stringify;
|
||||
_load_web_plugins( setting('extra_web_plugins') );
|
||||
}
|
||||
|
||||
@@ -50,9 +55,18 @@ hook 'before_template' => sub {
|
||||
|
||||
# allow very long lists of ports
|
||||
$Template::Directive::WHILE_MAX = 10_000;
|
||||
|
||||
# allow hash keys with leading underscores
|
||||
$Template::Stash::PRIVATE = undef;
|
||||
};
|
||||
|
||||
get '/' => sub {
|
||||
if (var('user') and var('user')->admin) {
|
||||
if (schema('netdisco')->resultset('Device')->count == 0) {
|
||||
var('nodevices' => true);
|
||||
}
|
||||
}
|
||||
|
||||
template 'index';
|
||||
};
|
||||
|
||||
|
||||
80
Netdisco/lib/App/Netdisco/Web/AdminTask.pm
Normal file
80
Netdisco/lib/App/Netdisco/Web/AdminTask.pm
Normal file
@@ -0,0 +1,80 @@
|
||||
package App::Netdisco::Web::AdminTask;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use Try::Tiny;
|
||||
|
||||
sub add_job {
|
||||
my ($jobtype, $device) = @_;
|
||||
|
||||
if ($device) {
|
||||
$device = NetAddr::IP::Lite->new($device);
|
||||
return send_error('Bad device', 400)
|
||||
if ! $device or $device->addr eq '0.0.0.0';
|
||||
}
|
||||
|
||||
try {
|
||||
# job might already be in the queue, so this could die
|
||||
schema('netdisco')->resultset('Admin')->create({
|
||||
($device ? (device => $device->addr) : ()),
|
||||
action => $jobtype,
|
||||
status => 'queued',
|
||||
username => session('user'),
|
||||
userip => request->remote_address,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
# we have a separate list for jobs needing a device to avoid queueing
|
||||
# such a job when there's no device param (it could still be duff, tho).
|
||||
my %jobs = map { $_ => 1} qw/
|
||||
discover
|
||||
macsuck
|
||||
arpnip
|
||||
/;
|
||||
my %jobs_all = map {$_ => 1} qw/
|
||||
discoverall
|
||||
macwalk
|
||||
arpwalk
|
||||
/;
|
||||
|
||||
foreach my $jobtype (keys %jobs_all, keys %jobs) {
|
||||
ajax "/ajax/control/admin/$jobtype" => sub {
|
||||
send_error('Forbidden', 403)
|
||||
unless var('user')->admin;
|
||||
send_error('Missing device', 400)
|
||||
if exists $jobs{$jobtype} and not param('device');
|
||||
|
||||
add_job($jobtype, param('device'));
|
||||
};
|
||||
|
||||
post "/admin/$jobtype" => sub {
|
||||
send_error('Forbidden', 403)
|
||||
unless var('user')->admin;
|
||||
send_error('Missing device', 400)
|
||||
if exists $jobs{$jobtype} and not param('device');
|
||||
|
||||
add_job($jobtype, param('device'));
|
||||
redirect uri_for('/admin/jobqueue')->path_query;
|
||||
};
|
||||
}
|
||||
|
||||
get '/admin/*' => sub {
|
||||
my ($tag) = splat;
|
||||
|
||||
if (! eval { var('user')->admin }) {
|
||||
return redirect uri_for('/')->path_query;
|
||||
}
|
||||
|
||||
# trick the ajax into working as if this were a tabbed page
|
||||
params->{tab} = $tag;
|
||||
|
||||
var(nav => 'admin');
|
||||
template 'admintask', {
|
||||
task => setting('_admin_tasks')->{ $tag },
|
||||
};
|
||||
};
|
||||
|
||||
true;
|
||||
@@ -18,30 +18,31 @@ hook 'before' => sub {
|
||||
if (session('user') && session->id) {
|
||||
var(user => schema('netdisco')->resultset('User')
|
||||
->find(session('user')));
|
||||
|
||||
# really just for dev work, to quieten the logs
|
||||
var('user')->port_control(0) if setting('no_port_control');
|
||||
}
|
||||
};
|
||||
|
||||
post '/login' => sub {
|
||||
status(302);
|
||||
|
||||
if (param('username') and param('password')) {
|
||||
my $user = schema('netdisco')->resultset('User')->find(param('username'));
|
||||
|
||||
if ($user) {
|
||||
my $sum = Digest::MD5::md5_hex(param('password'));
|
||||
if (($sum and $user->password) and ($sum eq $user->password)) {
|
||||
session(user => $user->username);
|
||||
header(Location => uri_for('/inventory')->path_query());
|
||||
return;
|
||||
return redirect uri_for('/inventory')->path_query;
|
||||
}
|
||||
}
|
||||
}
|
||||
header(Location => uri_for('/', {failed => 1})->path_query());
|
||||
|
||||
redirect uri_for('/', {failed => 1})->path_query;
|
||||
};
|
||||
|
||||
get '/logout' => sub {
|
||||
session->destroy;
|
||||
status(302);
|
||||
header(Location => uri_for('/', {logout => 1})->path_query());
|
||||
redirect uri_for('/', {logout => 1})->path_query;
|
||||
};
|
||||
|
||||
true;
|
||||
|
||||
@@ -5,10 +5,12 @@ use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
hook 'before' => sub {
|
||||
# list of port detail columns
|
||||
var('port_columns' => [
|
||||
{ name => 'c_admin', label => 'Admin Controls', default => '' },
|
||||
my @default_port_columns_left = (
|
||||
{ name => 'c_admin', label => 'Port Controls', default => '' },
|
||||
{ name => 'c_port', label => 'Port', default => 'on' },
|
||||
);
|
||||
|
||||
my @default_port_columns_right = (
|
||||
{ name => 'c_descr', label => 'Description', default => '' },
|
||||
{ name => 'c_type', label => 'Type', default => '' },
|
||||
{ name => 'c_duplex', label => 'Duplex', default => '' },
|
||||
@@ -24,7 +26,21 @@ hook 'before' => sub {
|
||||
{ name => 'c_neighbors', label => 'Connected Devices', default => 'on' },
|
||||
{ name => 'c_stp', label => 'Spanning Tree', default => '' },
|
||||
{ name => 'c_up', label => 'Status', default => '' },
|
||||
]);
|
||||
);
|
||||
|
||||
# build list of port detail columns
|
||||
my @port_columns = ();
|
||||
|
||||
push @port_columns,
|
||||
grep {$_->{position} eq 'left'} @{ setting('_extra_device_port_cols') };
|
||||
push @port_columns, @default_port_columns_left;
|
||||
push @port_columns,
|
||||
grep {$_->{position} eq 'mid'} @{ setting('_extra_device_port_cols') };
|
||||
push @port_columns, @default_port_columns_right;
|
||||
push @port_columns,
|
||||
grep {$_->{position} eq 'right'} @{ setting('_extra_device_port_cols') };
|
||||
|
||||
var('port_columns' => \@port_columns);
|
||||
|
||||
# view settings for port connected devices
|
||||
var('connected_properties' => [
|
||||
@@ -100,9 +116,7 @@ get '/device' => sub {
|
||||
});
|
||||
|
||||
if (!defined $dev) {
|
||||
status(302);
|
||||
header(Location => uri_for('/', {nosuchdevice => 1})->path_query());
|
||||
return;
|
||||
return redirect uri_for('/', {nosuchdevice => 1})->path_query;
|
||||
}
|
||||
|
||||
params->{'tab'} ||= 'details';
|
||||
|
||||
@@ -4,12 +4,16 @@ use Dancer ':syntax';
|
||||
use Dancer::Plugin;
|
||||
|
||||
set(
|
||||
'navbar_items' => [],
|
||||
'search_tabs' => [],
|
||||
'device_tabs' => [],
|
||||
'reports_menu' => {},
|
||||
'reports' => {},
|
||||
'report_order' => [qw/Device Port Node VLAN Network Wireless/],
|
||||
'_additional_css' => [],
|
||||
'_additional_javascript' => [],
|
||||
'_extra_device_port_cols' => [],
|
||||
'_navbar_items' => [],
|
||||
'_search_tabs' => [],
|
||||
'_device_tabs' => [],
|
||||
'_admin_tasks' => {},
|
||||
'_reports_menu' => {},
|
||||
'_reports' => {},
|
||||
'_report_order' => [qw/Device Port Node VLAN Network Wireless/],
|
||||
);
|
||||
|
||||
# this is what Dancer::Template::TemplateToolkit does by default
|
||||
@@ -19,8 +23,7 @@ register 'register_template_path' => sub {
|
||||
my ($self, $path) = plugin_args(@_);
|
||||
|
||||
if (!length $path) {
|
||||
error "bad template path to register_template_paths";
|
||||
return;
|
||||
return error "bad template path to register_template_paths";
|
||||
}
|
||||
|
||||
unshift
|
||||
@@ -28,6 +31,49 @@ register 'register_template_path' => sub {
|
||||
$path;
|
||||
};
|
||||
|
||||
sub _register_include {
|
||||
my ($type, $plugin) = @_;
|
||||
|
||||
if (!length $type) {
|
||||
return error "bad type to _register_include";
|
||||
}
|
||||
|
||||
if (!length $plugin) {
|
||||
return error "bad plugin name to register_$type";
|
||||
}
|
||||
|
||||
push @{ setting("_additional_$type") }, $plugin;
|
||||
}
|
||||
|
||||
register 'register_css' => sub {
|
||||
my ($self, $plugin) = plugin_args(@_);
|
||||
_register_include('css', $plugin);
|
||||
};
|
||||
|
||||
register 'register_javascript' => sub {
|
||||
my ($self, $plugin) = plugin_args(@_);
|
||||
_register_include('javascript', $plugin);
|
||||
};
|
||||
|
||||
register 'register_device_port_column' => sub {
|
||||
my ($self, $config) = plugin_args(@_);
|
||||
$config->{default} ||= '';
|
||||
$config->{position} ||= 'right';
|
||||
|
||||
if (!length $config->{name} or !length $config->{label}) {
|
||||
return error "bad config to register_device_port_column";
|
||||
}
|
||||
|
||||
foreach my $item (@{ setting('_extra_device_port_cols') }) {
|
||||
if ($item->{name} eq $config->{name}) {
|
||||
$item = $config;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
push @{ setting('_extra_device_port_cols') }, $config;
|
||||
};
|
||||
|
||||
register 'register_navbar_item' => sub {
|
||||
my ($self, $config) = plugin_args(@_);
|
||||
|
||||
@@ -35,29 +81,39 @@ register 'register_navbar_item' => sub {
|
||||
or !length $config->{path}
|
||||
or !length $config->{label}) {
|
||||
|
||||
error "bad config to register_navbar_item";
|
||||
return;
|
||||
return error "bad config to register_navbar_item";
|
||||
}
|
||||
|
||||
foreach my $item (@{ setting('navbar_items') }) {
|
||||
foreach my $item (@{ setting('_navbar_items') }) {
|
||||
if ($item->{tag} eq $config->{tag}) {
|
||||
$item = $config;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
push @{ setting('navbar_items') }, $config;
|
||||
push @{ setting('_navbar_items') }, $config;
|
||||
};
|
||||
|
||||
sub _register_tab {
|
||||
my ($nav, $config) = @_;
|
||||
my $stash = setting("${nav}_tabs");
|
||||
register 'register_admin_task' => sub {
|
||||
my ($self, $config) = plugin_args(@_);
|
||||
|
||||
if (!length $config->{tag}
|
||||
or !length $config->{label}) {
|
||||
|
||||
error "bad config to register_${nav}_item";
|
||||
return;
|
||||
return error "bad config to register_admin_task";
|
||||
}
|
||||
|
||||
setting('_admin_tasks')->{ $config->{tag} } = $config;
|
||||
};
|
||||
|
||||
sub _register_tab {
|
||||
my ($nav, $config) = @_;
|
||||
my $stash = setting("_${nav}_tabs");
|
||||
|
||||
if (!length $config->{tag}
|
||||
or !length $config->{label}) {
|
||||
|
||||
return error "bad config to register_${nav}_item";
|
||||
}
|
||||
|
||||
foreach my $item (@{ $stash }) {
|
||||
@@ -82,26 +138,25 @@ register 'register_device_tab' => sub {
|
||||
|
||||
register 'register_report' => sub {
|
||||
my ($self, $config) = plugin_args(@_);
|
||||
my @categories = @{ setting('report_order') };
|
||||
my @categories = @{ setting('_report_order') };
|
||||
|
||||
if (!length $config->{category}
|
||||
or !length $config->{tag}
|
||||
or !length $config->{label}
|
||||
or 0 == scalar grep {$config->{category} eq $_} @categories) {
|
||||
|
||||
error "bad config to register_report";
|
||||
return;
|
||||
return error "bad config to register_report";
|
||||
}
|
||||
|
||||
foreach my $item (@{setting('reports_menu')->{ $config->{category} }}) {
|
||||
foreach my $item (@{setting('_reports_menu')->{ $config->{category} }}) {
|
||||
if ($item eq $config->{tag}) {
|
||||
setting('reports')->{$config->{tag}} = $config;
|
||||
setting('_reports')->{$config->{tag}} = $config;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
push @{setting('reports_menu')->{ $config->{category} }}, $config->{tag};
|
||||
setting('reports')->{$config->{tag}} = $config;
|
||||
push @{setting('_reports_menu')->{ $config->{category} }}, $config->{tag};
|
||||
setting('_reports')->{$config->{tag}} = $config;
|
||||
};
|
||||
|
||||
register_plugin;
|
||||
|
||||
37
Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm
Normal file
37
Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm
Normal file
@@ -0,0 +1,37 @@
|
||||
package App::Netdisco::Web::Plugin::AdminTask::JobQueue;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use App::Netdisco::Web::Plugin;
|
||||
|
||||
register_admin_task({
|
||||
tag => 'jobqueue',
|
||||
label => 'Job Queue',
|
||||
});
|
||||
|
||||
ajax '/ajax/control/admin/jobqueue/del' => sub {
|
||||
send_error('Forbidden', 403) unless var('user')->admin;
|
||||
send_error('Missing job', 400) unless length param('job');
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $device = schema('netdisco')->resultset('Admin')
|
||||
->search({job => param('job')})->delete;
|
||||
});
|
||||
};
|
||||
|
||||
ajax '/ajax/content/admin/jobqueue' => sub {
|
||||
send_error('Forbidden', 403) unless var('user')->admin;
|
||||
|
||||
my $set = schema('netdisco')->resultset('Admin')
|
||||
->with_times
|
||||
->search({}, {order_by => { -desc => [qw/entered device action/] }});
|
||||
|
||||
content_type('text/html');
|
||||
template 'ajax/admintask/jobqueue.tt', {
|
||||
results => $set,
|
||||
}, { layout => undef };
|
||||
};
|
||||
|
||||
true;
|
||||
103
Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm
Normal file
103
Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm
Normal file
@@ -0,0 +1,103 @@
|
||||
package App::Netdisco::Web::Plugin::AdminTask::PseudoDevice;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use App::Netdisco::Web::Plugin;
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
|
||||
register_admin_task({
|
||||
tag => 'pseudodevice',
|
||||
label => 'Pseudo Devices',
|
||||
});
|
||||
|
||||
sub _sanity_ok {
|
||||
return 0 unless var('user') and var('user')->admin;
|
||||
|
||||
return 0 unless length param('dns')
|
||||
and param('dns') =~ m/^[[:print:]]+$/
|
||||
and param('dns') !~ m/[[:space:]]/;
|
||||
|
||||
my $ip = NetAddr::IP::Lite->new(param('ip'));
|
||||
return 0 unless ($ip and$ip->addr ne '0.0.0.0');
|
||||
|
||||
return 0 unless length param('ports')
|
||||
and param('ports') =~ m/^[[:digit:]]+$/;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
ajax '/ajax/control/admin/pseudodevice/add' => sub {
|
||||
send_error('Bad Request', 400) unless _sanity_ok();
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->create({
|
||||
ip => param('ip'),
|
||||
dns => param('dns'),
|
||||
vendor => 'netdisco',
|
||||
last_discover => \'now()',
|
||||
});
|
||||
return unless $device;
|
||||
|
||||
$device->ports->populate([
|
||||
['port'],
|
||||
map {["Port$_"]} @{[1 .. param('ports')]},
|
||||
]);
|
||||
});
|
||||
};
|
||||
|
||||
ajax '/ajax/control/admin/pseudodevice/del' => sub {
|
||||
send_error('Bad Request', 400) unless _sanity_ok();
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->find({ip => param('ip')});
|
||||
|
||||
$device->ports->delete;
|
||||
$device->delete;
|
||||
});
|
||||
};
|
||||
|
||||
ajax '/ajax/control/admin/pseudodevice/update' => sub {
|
||||
send_error('Bad Request', 400) unless _sanity_ok();
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->with_port_count->find({ip => param('ip')});
|
||||
return unless $device;
|
||||
my $count = $device->port_count;
|
||||
|
||||
if (param('ports') > $count) {
|
||||
my $start = $count + 1;
|
||||
$device->ports->populate([
|
||||
['port'],
|
||||
map {["Port$_"]} @{[$start .. param('ports')]},
|
||||
]);
|
||||
}
|
||||
elsif (param('ports') < $count) {
|
||||
my $start = param('ports') + 1;
|
||||
$device->ports
|
||||
->single({port => "Port$_"})->delete
|
||||
for ($start .. $count);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ajax '/ajax/content/admin/pseudodevice' => sub {
|
||||
send_error('Forbidden', 403) unless var('user')->admin;
|
||||
|
||||
my $set = schema('netdisco')->resultset('Device')
|
||||
->search(
|
||||
{vendor => 'netdisco'},
|
||||
{order_by => { -desc => 'last_discover' }},
|
||||
)->with_port_count;
|
||||
|
||||
content_type('text/html');
|
||||
template 'ajax/admintask/pseudodevice.tt', {
|
||||
results => $set,
|
||||
}, { layout => undef };
|
||||
};
|
||||
|
||||
true;
|
||||
103
Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm
Normal file
103
Netdisco/lib/App/Netdisco/Web/Plugin/AdminTask/Topology.pm
Normal file
@@ -0,0 +1,103 @@
|
||||
package App::Netdisco::Web::Plugin::AdminTask::Topology;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use App::Netdisco::Web::Plugin;
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
|
||||
register_admin_task({
|
||||
tag => 'topology',
|
||||
label => 'Manual Device Topology',
|
||||
});
|
||||
|
||||
sub _sanity_ok {
|
||||
return 0 unless var('user') and var('user')->admin;
|
||||
|
||||
my $dev1 = NetAddr::IP::Lite->new(param('dev1'));
|
||||
return 0 unless ($dev1 and $dev1->addr ne '0.0.0.0');
|
||||
|
||||
my $dev2 = NetAddr::IP::Lite->new(param('dev2'));
|
||||
return 0 unless ($dev2 and $dev2->addr ne '0.0.0.0');
|
||||
|
||||
return 0 unless length param('port1');
|
||||
return 0 unless length param('port2');
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
ajax '/ajax/control/admin/topology/add' => sub {
|
||||
send_error('Bad Request', 400) unless _sanity_ok();
|
||||
|
||||
my $device = schema('netdisco')->resultset('Topology')
|
||||
->create({
|
||||
dev1 => param('dev1'),
|
||||
port1 => param('port1'),
|
||||
dev2 => param('dev2'),
|
||||
port2 => param('port2'),
|
||||
});
|
||||
|
||||
# re-set remote device details in affected ports
|
||||
# could fail for bad device or port names
|
||||
try {
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# only work on root_ips
|
||||
my $left = get_device(param('dev1'));
|
||||
my $right = get_device(param('dev2'));
|
||||
|
||||
# skip bad entries
|
||||
return unless ($left->in_storage and $right->in_storage);
|
||||
|
||||
$left->ports
|
||||
->single({port => param('port1')}, {for => 'update'})
|
||||
->update({
|
||||
remote_ip => param('dev2'),
|
||||
remote_port => param('port2'),
|
||||
remote_type => undef,
|
||||
remote_id => undef,
|
||||
is_uplink => \"true",
|
||||
manual_topo => \"true",
|
||||
});
|
||||
|
||||
$right->ports
|
||||
->single({port => param('port2')}, {for => 'update'})
|
||||
->update({
|
||||
remote_ip => param('dev1'),
|
||||
remote_port => param('port1'),
|
||||
remote_type => undef,
|
||||
remote_id => undef,
|
||||
is_uplink => \"true",
|
||||
manual_topo => \"true",
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
ajax '/ajax/control/admin/topology/del' => sub {
|
||||
send_error('Bad Request', 400) unless _sanity_ok();
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
my $device = schema('netdisco')->resultset('Topology')
|
||||
->search({
|
||||
dev1 => param('dev1'),
|
||||
port1 => param('port1'),
|
||||
dev2 => param('dev2'),
|
||||
port2 => param('port2'),
|
||||
})->delete;
|
||||
});
|
||||
};
|
||||
|
||||
ajax '/ajax/content/admin/topology' => sub {
|
||||
send_error('Forbidden', 403) unless var('user')->admin;
|
||||
|
||||
my $set = schema('netdisco')->resultset('Topology')
|
||||
->search({},{order_by => [qw/dev1 dev2 port1/]});
|
||||
|
||||
content_type('text/html');
|
||||
template 'ajax/admintask/topology.tt', {
|
||||
results => $set,
|
||||
}, { layout => undef };
|
||||
};
|
||||
|
||||
true;
|
||||
@@ -13,7 +13,7 @@ ajax '/ajax/content/device/addresses' => sub {
|
||||
my $q = param('q');
|
||||
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->search_for_device($q) or return;
|
||||
->search_for_device($q) or send_error('Bad device', 400);
|
||||
|
||||
my $set = $device->device_ips->search({}, {order_by => 'alias'});
|
||||
return unless $set->count;
|
||||
|
||||
@@ -12,7 +12,7 @@ register_device_tab({ tag => 'details', label => 'Details' });
|
||||
ajax '/ajax/content/device/details' => sub {
|
||||
my $q = param('q');
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->with_times()->search_for_device($q) or return;
|
||||
->with_times()->search_for_device($q) or send_error('Bad device', 400);
|
||||
|
||||
content_type('text/html');
|
||||
template 'ajax/device/details.tt', {
|
||||
|
||||
@@ -43,7 +43,7 @@ get '/ajax/data/device/netmap' => sub {
|
||||
my $q = param('q');
|
||||
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->search_for_device($q) or return;
|
||||
->search_for_device($q) or send_error('Bad device', 400);
|
||||
my $start = $device->ip;
|
||||
|
||||
my @devices = schema('netdisco')->resultset('Device')->search({}, {
|
||||
@@ -72,7 +72,7 @@ get '/ajax/data/device/netmap' => sub {
|
||||
_add_children($tree{children}, var('links')->{$start});
|
||||
|
||||
content_type('application/json');
|
||||
return to_json(\%tree);
|
||||
to_json(\%tree);
|
||||
};
|
||||
|
||||
ajax '/ajax/data/device/alldevicelinks' => sub {
|
||||
@@ -93,7 +93,7 @@ ajax '/ajax/data/device/alldevicelinks' => sub {
|
||||
}
|
||||
|
||||
content_type('application/json');
|
||||
return to_json(\%tree);
|
||||
to_json(\%tree);
|
||||
};
|
||||
|
||||
true;
|
||||
|
||||
@@ -14,7 +14,7 @@ ajax '/ajax/content/device/ports' => sub {
|
||||
my $q = param('q');
|
||||
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->search_for_device($q) or return;
|
||||
->search_for_device($q) or send_error('Bad device', 400);
|
||||
my $set = $device->ports;
|
||||
|
||||
# refine by ports if requested
|
||||
@@ -77,7 +77,7 @@ ajax '/ajax/content/device/ports' => sub {
|
||||
template 'ajax/device/ports.tt', {
|
||||
results => $results,
|
||||
nodes => $nodes_name,
|
||||
device => $device->ip,
|
||||
device => $device,
|
||||
}, { layout => undef };
|
||||
};
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ ajax '/ajax/content/search/device' => sub {
|
||||
}
|
||||
else {
|
||||
my $q = param('q');
|
||||
return unless $q;
|
||||
send_error('Missing query', 400) unless $q;
|
||||
|
||||
$set = schema('netdisco')->resultset('Device')->search_fuzzy($q);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ register_search_tab({ tag => 'node', label => 'Node' });
|
||||
# nodes matching the param as an IP or DNS hostname or MAC
|
||||
ajax '/ajax/content/search/node' => sub {
|
||||
my $node = param('q');
|
||||
return unless $node;
|
||||
send_error('Missing node', 400) unless $node;
|
||||
content_type('text/html');
|
||||
|
||||
my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0);
|
||||
|
||||
@@ -11,7 +11,7 @@ register_search_tab({ tag => 'port', label => 'Port' });
|
||||
# device ports with a description (er, name) matching
|
||||
ajax '/ajax/content/search/port' => sub {
|
||||
my $q = param('q');
|
||||
return unless $q;
|
||||
send_error('Missing query', 400) unless $q;
|
||||
my $set;
|
||||
|
||||
if ($q =~ m/^\d+$/) {
|
||||
|
||||
@@ -11,7 +11,7 @@ register_search_tab({ tag => 'vlan', label => 'VLAN' });
|
||||
# devices carrying vlan xxx
|
||||
ajax '/ajax/content/search/vlan' => sub {
|
||||
my $q = param('q');
|
||||
return unless $q;
|
||||
send_error('Missing query', 400) unless $q;
|
||||
my $set;
|
||||
|
||||
if ($q =~ m/^\d+$/) {
|
||||
|
||||
@@ -4,42 +4,43 @@ use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use Try::Tiny;
|
||||
|
||||
ajax '/ajax/portcontrol' => sub {
|
||||
try {
|
||||
my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]',
|
||||
param('device'), (param('port') || ''), param('field'),
|
||||
(param('action') || ''), (param('value') || '');
|
||||
send_error('Forbidden', 403)
|
||||
unless var('user')->port_control;
|
||||
send_error('No device/port/field', 400)
|
||||
unless param('device') and param('port') and param('field');
|
||||
|
||||
my %action_map = (
|
||||
'location' => 'location',
|
||||
'contact' => 'contact',
|
||||
'c_port' => 'portcontrol',
|
||||
'c_name' => 'portname',
|
||||
'c_vlan' => 'vlan',
|
||||
'c_power' => 'power',
|
||||
);
|
||||
my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]',
|
||||
param('device'), (param('port') || ''), param('field'),
|
||||
(param('action') || ''), (param('value') || '');
|
||||
|
||||
my $action = $action_map{ param('field') };
|
||||
my $subaction = ($action =~ m/^(?:power|portcontrol)/
|
||||
? (param('action') ."-other")
|
||||
: param('value'));
|
||||
my %action_map = (
|
||||
'location' => 'location',
|
||||
'contact' => 'contact',
|
||||
'c_port' => 'portcontrol',
|
||||
'c_name' => 'portname',
|
||||
'c_vlan' => 'vlan',
|
||||
'c_power' => 'power',
|
||||
);
|
||||
|
||||
schema('netdisco')->resultset('Admin')->create({
|
||||
device => param('device'),
|
||||
port => param('port'),
|
||||
action => $action,
|
||||
subaction => $subaction,
|
||||
status => 'queued',
|
||||
username => session('user'),
|
||||
userip => request->remote_address,
|
||||
log => $log,
|
||||
});
|
||||
}
|
||||
catch {
|
||||
send_error('Failed to parse params or add DB record');
|
||||
};
|
||||
send_error('No action/value', 400)
|
||||
unless (param('action') or param('value'));
|
||||
|
||||
my $action = $action_map{ param('field') };
|
||||
my $subaction = ($action =~ m/^(?:power|portcontrol)/
|
||||
? (param('action') ."-other")
|
||||
: param('value'));
|
||||
|
||||
schema('netdisco')->resultset('Admin')->create({
|
||||
device => param('device'),
|
||||
port => param('port'),
|
||||
action => $action,
|
||||
subaction => $subaction,
|
||||
status => 'queued',
|
||||
username => session('user'),
|
||||
userip => request->remote_address,
|
||||
log => $log,
|
||||
});
|
||||
|
||||
content_type('application/json');
|
||||
to_json({});
|
||||
@@ -47,7 +48,7 @@ ajax '/ajax/portcontrol' => sub {
|
||||
|
||||
ajax '/ajax/userlog' => sub {
|
||||
my $user = session('user');
|
||||
send_error('No username') unless $user;
|
||||
send_error('No username', 400) unless $user;
|
||||
|
||||
my $rs = schema('netdisco')->resultset('Admin')->search({
|
||||
username => $user,
|
||||
|
||||
@@ -10,7 +10,7 @@ get '/report/*' => sub {
|
||||
|
||||
var(nav => 'reports');
|
||||
template 'report', {
|
||||
report => setting('reports')->{ $tag },
|
||||
report => setting('_reports')->{ $tag },
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -65,9 +65,7 @@ get '/search' => sub {
|
||||
|
||||
if (not param('tab')) {
|
||||
if (not $q) {
|
||||
status(302);
|
||||
header(Location => uri_for('/')->path_query());
|
||||
return;
|
||||
return redirect uri_for('/')->path_query;
|
||||
}
|
||||
|
||||
# pick most likely tab for initial results
|
||||
@@ -80,13 +78,11 @@ get '/search' => sub {
|
||||
if ($nd and $nd->count) {
|
||||
if ($nd->count == 1) {
|
||||
# redirect to device details for the one device
|
||||
status(302);
|
||||
header(Location => uri_for('/device', {
|
||||
return redirect uri_for('/device', {
|
||||
tab => 'details',
|
||||
q => ($nd->first->dns || $nd->first->ip),
|
||||
f => '',
|
||||
})->path_query());
|
||||
return;
|
||||
})->path_query;
|
||||
}
|
||||
|
||||
# multiple devices
|
||||
|
||||
30
Netdisco/lib/App/Netdisco/Web/Static.pm
Normal file
30
Netdisco/lib/App/Netdisco/Web/Static.pm
Normal file
@@ -0,0 +1,30 @@
|
||||
package App::Netdisco::Web::Static;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Path::Class;
|
||||
|
||||
get '/plugin/*/*.js' => sub {
|
||||
my ($plugin) = splat;
|
||||
|
||||
my $content = template
|
||||
"plugin/$plugin/$plugin.js", {},
|
||||
{ layout => undef };
|
||||
|
||||
send_file \$content,
|
||||
content_type => 'application/javascript',
|
||||
filename => "$plugin.js";
|
||||
};
|
||||
|
||||
get '/plugin/*/*.css' => sub {
|
||||
my ($plugin) = splat;
|
||||
|
||||
my $content = template
|
||||
"plugin/$plugin/$plugin.css", {},
|
||||
{ layout => undef };
|
||||
|
||||
send_file \$content,
|
||||
content_type => 'text/css',
|
||||
filename => "$plugin.css";
|
||||
};
|
||||
|
||||
true;
|
||||
@@ -4,13 +4,51 @@ use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
# support typeahead with simple AJAX query for device names
|
||||
ajax '/ajax/data/device/typeahead' => sub {
|
||||
my $q = param('query');
|
||||
use App::Netdisco::Util::Web (); # for sort_port
|
||||
|
||||
ajax '/ajax/data/devicename/typeahead' => sub {
|
||||
my $q = param('query') || param('term');
|
||||
my $set = schema('netdisco')->resultset('Device')->search_fuzzy($q);
|
||||
|
||||
content_type 'application/json';
|
||||
return to_json [map {$_->dns || $_->name || $_->ip} $set->all];
|
||||
to_json [map {$_->dns || $_->name || $_->ip} $set->all];
|
||||
};
|
||||
|
||||
ajax '/ajax/data/deviceip/typeahead' => sub {
|
||||
my $q = param('query') || param('term');
|
||||
my $set = schema('netdisco')->resultset('Device')->search_fuzzy($q);
|
||||
|
||||
my @data = ();
|
||||
while (my $d = $set->next) {
|
||||
my $label = $d->ip;
|
||||
if ($d->dns or $d->name) {
|
||||
$label = sprintf '%s (%s)',
|
||||
($d->dns || $d->name), $d->ip;
|
||||
}
|
||||
push @data, {label => $label, value => $d->ip};
|
||||
}
|
||||
|
||||
content_type 'application/json';
|
||||
to_json \@data;
|
||||
};
|
||||
|
||||
ajax '/ajax/data/port/typeahead' => sub {
|
||||
my $dev = param('dev1') || param('dev2');
|
||||
my $port = param('port1') || param('port2');
|
||||
send_error('Missing device', 400) unless length $dev;
|
||||
|
||||
my $device = schema('netdisco')->resultset('Device')
|
||||
->find({ip => $dev});
|
||||
send_error('Bad device', 400) unless $device;
|
||||
|
||||
my $set = $device->ports({},{order_by => 'port'});
|
||||
$set = $set->search({port => { -ilike => "\%$port\%" }})
|
||||
if length $port;
|
||||
|
||||
my $results = [ sort { &App::Netdisco::Util::Web::sort_port($a->port, $b->port) } $set->all ];
|
||||
|
||||
content_type 'application/json';
|
||||
to_json [map {$_->port} @$results];
|
||||
};
|
||||
|
||||
true;
|
||||
|
||||
21
Netdisco/lib/App/NetdiscoX/Web/Plugin/Observium.pm
Normal file
21
Netdisco/lib/App/NetdiscoX/Web/Plugin/Observium.pm
Normal file
@@ -0,0 +1,21 @@
|
||||
package App::NetdiscoX::Web::Plugin::Observium;
|
||||
|
||||
use Dancer ':syntax';
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use App::Netdisco::Web::Plugin;
|
||||
|
||||
use File::ShareDir 'dist_dir';
|
||||
use Path::Class;
|
||||
|
||||
register_device_port_column({
|
||||
name => 'observium',
|
||||
position => 'mid',
|
||||
label => 'Traffic',
|
||||
default => 'on',
|
||||
});
|
||||
|
||||
register_css('observium');
|
||||
register_javascript('observium');
|
||||
|
||||
true;
|
||||
Reference in New Issue
Block a user