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
@@ -2,10 +2,19 @@
|
||||
|
||||
[NEW FEATURES]
|
||||
|
||||
* Finally we have a discover/refresh daemon job :)
|
||||
* Finally we have a discover/refresh/arpnip/macsuck daemon jobs :)
|
||||
* Also... a Scheduler which removes need for crontab installation
|
||||
* The netdisco-do script can run a one-off discover for a device
|
||||
* Can select MAC Address display format on Node and Device Port search
|
||||
* The netdisco-do script can queue any one-off job
|
||||
* Select MAC Address display format on Node and Device Port search
|
||||
* Helper script to import the Netdisco 1.x Topology file to the database
|
||||
* Support for pseudo devices (useful for dummy device links)
|
||||
* Manual Topology editing via the web
|
||||
* Job Queue view and delete page
|
||||
* Empty device table prompts initial discover on homepage
|
||||
* Support for App::NetdiscoX::Web::Plugin namespace
|
||||
* Plugins can add columns to Device Ports display
|
||||
* Observium Sparklines port column plugin
|
||||
* Plugins can have CSS and Javascript loaded within <head>
|
||||
|
||||
[ENHANCEMENTS]
|
||||
|
||||
@@ -14,12 +23,20 @@
|
||||
* Port filter in device port display is now highlighted green
|
||||
* Navbar search is fuzzier
|
||||
* Phone node icon is a little phone handset
|
||||
* User icon color indicates port_control/admin ability
|
||||
* Buttons for discover/macsuck/arpnip on device details page
|
||||
* Support 'path' config option as alternative to --path /mountpoint
|
||||
* Local plugins can be placed in ${NETDISCO_HOME}/site_plugins/...
|
||||
* Missing mibdirs causes all MIBs to be loaded (with a warning)
|
||||
|
||||
[BUG FIXES]
|
||||
|
||||
* Rename plugins developer doc to .pod
|
||||
* Update to latest Bootstrap and JQuery, and temp. fix #7326 in Bootstrap
|
||||
* Partial Name in Port search now working
|
||||
* Add unique constraints to topology table
|
||||
* Handle whitespace ahead of OUI data
|
||||
* Wasn't using Bootstrap table class properly
|
||||
|
||||
2.007000_001 - 2013-03-17
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ requires 'HTML::Parser' => 3.70;
|
||||
requires 'HTTP::Tiny' => 0.029;
|
||||
requires 'JSON' => 0;
|
||||
requires 'List::MoreUtils' => 0.33;
|
||||
requires 'MIME::Base64' => 3.13;
|
||||
requires 'Moo' => 1.001000;
|
||||
requires 'MCE' => 1.408;
|
||||
requires 'Net::DNS' => 0.72;
|
||||
@@ -32,7 +33,7 @@ requires 'Socket6' => 0.23;
|
||||
requires 'Starman' => 0.3008;
|
||||
requires 'SNMP::Info' => 3.01;
|
||||
requires 'SQL::Translator' => 0.11016;
|
||||
requires 'Template::Toolkit' => 2.24;
|
||||
requires 'Template' => 2.24;
|
||||
requires 'YAML' => 0.84;
|
||||
requires 'namespace::clean' => 0.24;
|
||||
requires 'version' => 0.9902;
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use App::Netdisco;
|
||||
FindBin::again();
|
||||
use Path::Class 'dir';
|
||||
|
||||
BEGIN {
|
||||
# stuff useful locations into @INC
|
||||
unshift @INC,
|
||||
dir($FindBin::RealBin)->parent->subdir('lib')->stringify,
|
||||
dir($FindBin::RealBin, 'lib')->stringify;
|
||||
}
|
||||
|
||||
use App::Netdisco;
|
||||
use Dancer ':script';
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
|
||||
99
Netdisco/bin/nd-import-topology
Executable file
@@ -0,0 +1,99 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use FindBin;
|
||||
FindBin::again();
|
||||
use Path::Class 'dir';
|
||||
|
||||
BEGIN {
|
||||
# stuff useful locations into @INC
|
||||
unshift @INC,
|
||||
dir($FindBin::RealBin)->parent->subdir('lib')->stringify,
|
||||
dir($FindBin::RealBin, 'lib')->stringify;
|
||||
}
|
||||
|
||||
use App::Netdisco;
|
||||
use Dancer ':script';
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::Device 'get_device';
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
use Try::Tiny;
|
||||
|
||||
=head1 NAME
|
||||
|
||||
nd-import-topology - Import a Nedisco 1.x Manual Topology File
|
||||
|
||||
=head2 USAGE
|
||||
|
||||
./nd-import-topology /path/to/netdisco-topology.txt
|
||||
|
||||
=head2 DESCRIPTION
|
||||
|
||||
This helper script will read and import the content of a Netdisco 1.x format
|
||||
Manual Topology file into the Netdisco 2.x database's C<topology> table.
|
||||
|
||||
It's safe to run the script multiple times on the same file - any new data
|
||||
will be imported.
|
||||
|
||||
The file syntax must be like so:
|
||||
|
||||
left-device
|
||||
link:left-port,right-device,right-port
|
||||
|
||||
The devices can be either host names or IPs. Data will be imported even if the
|
||||
devices are currently unknown to Netdisco.
|
||||
|
||||
=cut
|
||||
|
||||
my $file = $ARGV[0];
|
||||
die "missing topology file name on command line\n" unless $file;
|
||||
|
||||
chomp $file;
|
||||
my $dev = undef; # current device
|
||||
print "Loading topology information from $file\n";
|
||||
|
||||
open (DEVS,'<', $file)
|
||||
or die "topo_load_file($file): $!\n";
|
||||
|
||||
while (my $line = <DEVS>) {
|
||||
chomp $line;
|
||||
$line =~ s/(?<!\\)#.*//;
|
||||
$line =~ s/\\#/#/g;
|
||||
$line =~ s/^\s+//g;
|
||||
$line =~ s/\s+$//g;
|
||||
next if $line =~ m/^\s*$/;
|
||||
|
||||
if ($line =~ m/^link:(.*)/){
|
||||
my ($from_port, $to, $to_port) = split(m/,/, $1);
|
||||
|
||||
unless (defined $dev) {
|
||||
print " Skipping $line. No device yet defined!\n";
|
||||
next;
|
||||
}
|
||||
|
||||
# save Link info
|
||||
try {
|
||||
schema('netdisco')->txn_do(sub {
|
||||
schema('netdisco')->resultset('Topology')->create({
|
||||
dev1 => $dev,
|
||||
port1 => $from_port,
|
||||
dev2 => get_device($to)->ip,
|
||||
port2 => $to_port,
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
elsif ($line =~ /^alias:(.*)/) {
|
||||
# ignore aliases
|
||||
}
|
||||
else {
|
||||
my $ip = NetAddr::IP::Lite->new($line)
|
||||
or next;
|
||||
next if $ip->addr eq '0.0.0.0';
|
||||
|
||||
$dev = get_device($ip->addr)->ip;
|
||||
print " Set device: $dev\n";
|
||||
}
|
||||
}
|
||||
|
||||
close (DEVS);
|
||||
@@ -1,9 +1,17 @@
|
||||
#!/usr/bin/env perl
|
||||
|
||||
use FindBin;
|
||||
use lib "$FindBin::Bin/../lib";
|
||||
use App::Netdisco;
|
||||
FindBin::again();
|
||||
use Path::Class 'dir';
|
||||
|
||||
BEGIN {
|
||||
# stuff useful locations into @INC
|
||||
unshift @INC,
|
||||
dir($FindBin::RealBin)->parent->subdir('lib')->stringify,
|
||||
dir($FindBin::RealBin, 'lib')->stringify;
|
||||
}
|
||||
|
||||
use App::Netdisco;
|
||||
use Dancer ':script';
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
@@ -69,7 +77,8 @@ try {
|
||||
};
|
||||
|
||||
# upgrade from whatever dbix_class_schema_versions says, to $VERSION
|
||||
my $db_version = $schema->get_db_version;
|
||||
# except that get_db_version will be 0 at first deploy
|
||||
my $db_version = ($schema->get_db_version || 1);
|
||||
my $target_version = $schema->schema_version;
|
||||
|
||||
# one step at a time, in case user has applied local changes already
|
||||
|
||||
@@ -134,7 +134,7 @@ sub deploy_oui {
|
||||
|
||||
if ($resp->{success}) {
|
||||
foreach my $line (split /\n/, $resp->{content}) {
|
||||
if ($line =~ m/^(.{2}-.{2}-.{2})\s+\(hex\)\s+(.*)\s*$/i) {
|
||||
if ($line =~ m/^\s*(.{2}-.{2}-.{2})\s+\(hex\)\s+(.*)\s*$/i) {
|
||||
my ($oui, $company) = ($1, $2);
|
||||
$oui =~ s/-/:/g;
|
||||
$data{lc($oui)} = $company;
|
||||
|
||||
@@ -50,6 +50,8 @@ if (!length $action) {
|
||||
package MyWorker;
|
||||
use Moo;
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Device';
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Arpnip';
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Macsuck';
|
||||
}
|
||||
my $worker = MyWorker->new();
|
||||
|
||||
|
||||
@@ -34,7 +34,16 @@ set plack_middlewares => [
|
||||
];
|
||||
|
||||
use App::Netdisco::Web;
|
||||
dance;
|
||||
use Plack::Builder;
|
||||
|
||||
my $app = sub {
|
||||
my $env = shift;
|
||||
my $request = Dancer::Request->new(env => $env);
|
||||
Dancer->dance($request);
|
||||
};
|
||||
|
||||
my $path = (setting('path') || '/');
|
||||
builder { mount $path => $app };
|
||||
|
||||
=head1 NAME
|
||||
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
_enqueue_discover($n, $remote_type);
|
||||
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,38 +710,113 @@ 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",
|
||||
});
|
||||
}
|
||||
|
||||
return @to_discover;
|
||||
}
|
||||
|
||||
# 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) = @_;
|
||||
|
||||
schema('netdisco')->txn_do(sub {
|
||||
# clear manual topology flags
|
||||
schema('netdisco')->resultset('DevicePort')->update({manual_topo => \'false'});
|
||||
|
||||
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",
|
||||
});
|
||||
|
||||
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);
|
||||
$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
|
||||
sub _enqueue_discover {
|
||||
my ($ip, $remote_type) = @_;
|
||||
foreach my $neighbor (@to_discover) {
|
||||
my ($ip, $remote_type) = @$neighbor;
|
||||
|
||||
my $device = get_device($ip);
|
||||
return if $device->in_storage;
|
||||
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;
|
||||
return;
|
||||
next;
|
||||
}
|
||||
|
||||
try {
|
||||
# could fail if queued job already exists
|
||||
try {
|
||||
schema('netdisco')->resultset('Admin')->create({
|
||||
device => $ip,
|
||||
action => 'discover',
|
||||
@@ -726,5 +824,6 @@ sub _enqueue_discover {
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
1;
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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([
|
||||
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',
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,10 +4,12 @@ use Dancer ':syntax';
|
||||
use Dancer::Plugin::Ajax;
|
||||
use Dancer::Plugin::DBIC;
|
||||
|
||||
use Try::Tiny;
|
||||
|
||||
ajax '/ajax/portcontrol' => sub {
|
||||
try {
|
||||
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 $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]',
|
||||
param('device'), (param('port') || ''), param('field'),
|
||||
(param('action') || ''), (param('value') || '');
|
||||
@@ -21,6 +23,9 @@ ajax '/ajax/portcontrol' => sub {
|
||||
'c_power' => 'power',
|
||||
);
|
||||
|
||||
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")
|
||||
@@ -36,10 +41,6 @@ ajax '/ajax/portcontrol' => sub {
|
||||
userip => request->remote_address,
|
||||
log => $log,
|
||||
});
|
||||
}
|
||||
catch {
|
||||
send_error('Failed to parse params or add DB record');
|
||||
};
|
||||
|
||||
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
@@ -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
@@ -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;
|
||||
@@ -38,6 +38,9 @@ engines:
|
||||
web_plugins:
|
||||
- Inventory
|
||||
- Report::DuplexMismatch
|
||||
- AdminTask::PseudoDevice
|
||||
- AdminTask::Topology
|
||||
- AdminTask::JobQueue
|
||||
- Search::Device
|
||||
- Search::Node
|
||||
- Search::VLAN
|
||||
|
||||
2
Netdisco/share/public/css/jquery.qtip.min.css
vendored
Normal file
@@ -2,11 +2,11 @@ body {
|
||||
padding-top: 0px !important;
|
||||
}
|
||||
|
||||
#search_results > li:not(.active) {
|
||||
#nd_search-results > li:not(.active) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navbar, .sidebar {
|
||||
.navbar, .nd_sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* for the fixed navbar make sure content stops short of page top*/
|
||||
/* style common to all pages in the site */
|
||||
|
||||
/* for the fixed navbar make sure content stops short of page top*/
|
||||
body {
|
||||
padding-top: 50px;
|
||||
}
|
||||
|
||||
/* magnifying glass icon for search box */
|
||||
.navbar_icon {
|
||||
.nd_navbar-icon {
|
||||
vertical-align: sub;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* for the "logged in as..." text */
|
||||
.nd_navbartext {
|
||||
.nd_navbar-text {
|
||||
color: #666;
|
||||
padding-top: 11px;
|
||||
}
|
||||
@@ -27,45 +28,36 @@ body {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* various styles to adjust the hero box used for homepage + login */
|
||||
/* jquery ui autocomplete scrollable */
|
||||
.ui-autocomplete {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.nd_herorow {
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* styles to adjust the hero box used for homepage + login */
|
||||
|
||||
/* space between hero box and navbar */
|
||||
.nd_hero-row {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
/* alter proportions of hero unit to make it "tighter" on content */
|
||||
.hero-unit {
|
||||
padding: 30px 60px 40px 90px;
|
||||
}
|
||||
|
||||
.nd_loginform {
|
||||
/* push user/pass/login form down+away from the Netdisco banner text */
|
||||
.nd_login-form {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* styles for Reports */
|
||||
/* styles for device inventory */
|
||||
|
||||
/* from Bootstrap doc style sheet */
|
||||
.nd_show-grid [class*="span"] {
|
||||
background-color: cornsilk;
|
||||
text-align: center;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
min-height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* styles for Inventory */
|
||||
|
||||
#nd_dev_age_form {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.nd_inv_tbl_head {
|
||||
.nd_inventory-table-head {
|
||||
text-align: center;
|
||||
color: lightSlateGray;
|
||||
margin-top: 6px;
|
||||
@@ -73,12 +65,7 @@ body {
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* results table links */
|
||||
|
||||
.nd_stealthlink {
|
||||
text-decoration: none !important;
|
||||
color: #404040;
|
||||
}
|
||||
/* styles for links in results tables */
|
||||
|
||||
/* make the whole cell become a hyperlink in results table */
|
||||
.nd_linkcell {
|
||||
@@ -87,110 +74,65 @@ body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* special placing for edit icon in details tab */
|
||||
.nd_device_details_edit {
|
||||
float: right !important;
|
||||
font-size: 14px;
|
||||
/* still a link, but styled like normal text */
|
||||
.nd_stealth-link {
|
||||
text-decoration: none !important;
|
||||
color: #404040;
|
||||
}
|
||||
|
||||
/* port admin up/down control */
|
||||
.nd_edit_icon, .nd_hand_icon {
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nd_power_icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-off {
|
||||
vertical-align: middle;
|
||||
color: darkRed;
|
||||
}
|
||||
|
||||
.nd_power_on {
|
||||
color: darkGreen;
|
||||
}
|
||||
|
||||
/* placement of port link when port admin hint is enabled */
|
||||
.nd_editable_cell > .nd_this_port_only {
|
||||
/* nudge cell content to the right when port_control controls are enabled */
|
||||
.nd_editable-cell > .nd_this-port-only {
|
||||
margin-left: 18px;
|
||||
}
|
||||
.nd_editable_cell > .nd_editable_cell_content {
|
||||
.nd_editable-cell > .nd_editable-cell-content {
|
||||
margin-left: 18px;
|
||||
}
|
||||
|
||||
/* style of editable content in table */
|
||||
[contenteditable]:focus {
|
||||
background: #FFFFD3 !important;
|
||||
.table .nd_nudge-for-icon {
|
||||
padding-left: 25px;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* many styles for the collapsing lists */
|
||||
/* styles to position table cell content */
|
||||
|
||||
/* mouse-over should be pointer to show JS collapser is clickable */
|
||||
.nd_collapser {
|
||||
cursor: pointer;
|
||||
color: #0088CC;
|
||||
}
|
||||
|
||||
/* collapser label should not have any decoration even though it's clickable */
|
||||
.clearfix > a {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* collapser label should not have any decoration even though it's clickable */
|
||||
.nd_collapse_vlans {
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
color: #0088CC;
|
||||
}
|
||||
|
||||
/* class to control default state of collapsible lists on page load */
|
||||
.nd_collapse_pre_hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* for the tagged vlans total when hiding the full list */
|
||||
.vlan_total {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* little up/down chevron to the right of some collapser link */
|
||||
.arrow-up-down {
|
||||
float: right;
|
||||
margin-top: 1px;
|
||||
margin-right: 1px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* draw little up arrow to the left of a label for collapsed list */
|
||||
.cell-arrow-up-down {
|
||||
float: left;
|
||||
margin-right: 6px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* for table and to position cell content */
|
||||
|
||||
td {
|
||||
.table td {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.center_cell {
|
||||
.table .nd_center-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* tabs */
|
||||
/* fix layout of form fields inside the (topology) table */
|
||||
td div.input-append {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#search_results {
|
||||
/* admin buttons in the device details view */
|
||||
td > form.nd_inline-form {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* fix layout of form fields inside the (pseudo devices) table */
|
||||
.nd_center-cell input {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* with two forms inside one cell, make the submit buttons side-by-side */
|
||||
.nd_inline-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* styles for "tabs" and surrounding content */
|
||||
|
||||
|
||||
/* add a small bottom margin (gutter) below all pages */
|
||||
#nd_search-results {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#nd_device_name {
|
||||
/* for any label which we want to appear alongside tabs, floated to the right */
|
||||
#nd_device-name {
|
||||
float: right;
|
||||
margin-bottom: 0px;
|
||||
margin-top: 9px;
|
||||
@@ -198,159 +140,131 @@ td {
|
||||
color: #6D5720;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* style customization for many items which appear in the sidebar */
|
||||
/* when there's only one tab (report, task etc) change the text color */
|
||||
.nd_single-tab {
|
||||
color: rgb(187,112,0) !important;
|
||||
}
|
||||
|
||||
/* fixups for prepended checkbox in sidebar */
|
||||
.nd_searchcheckbox {
|
||||
width: 123px;
|
||||
padding-left: 8px;
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* style for port_control controls */
|
||||
|
||||
/* edit icon in details tab is in the label (not content) cell so nudge to RHS*/
|
||||
.nd_device-details-edit {
|
||||
float: right !important;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* port admin up/down control */
|
||||
.nd_edit-icon, .nd_hand-icon {
|
||||
cursor: pointer;
|
||||
float: left;
|
||||
display: none;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* port power control */
|
||||
.nd_power-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* for some reason bootstrap 2.1.0 displays add-on as block - no check supprt? */
|
||||
.nd_checkboxlabel {
|
||||
display: inline;
|
||||
/* the port power icon, whether it's on or off */
|
||||
.icon-off {
|
||||
vertical-align: middle;
|
||||
color: darkRed;
|
||||
}
|
||||
|
||||
/* fixups for placing the Archived "A" inside the prepended checkbox */
|
||||
.nd_legendlabel {
|
||||
/* change color of icon from default of red (which is OK for power-off) */
|
||||
.nd_power-on {
|
||||
color: darkGreen;
|
||||
}
|
||||
|
||||
/* style of editable content in any table - yellow background */
|
||||
[contenteditable]:focus {
|
||||
background: #FFFFD3 !important;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* styles for collapsing lists - sidebar or main table cell content */
|
||||
|
||||
/* sidebar collapser is clickable and deep grey */
|
||||
.nd_collapser {
|
||||
cursor: pointer;
|
||||
color: #0088CC;
|
||||
}
|
||||
|
||||
/* vlans collapser also clickable and deep grey but with no link styling */
|
||||
.nd_collapse-vlans {
|
||||
cursor: pointer;
|
||||
color: #0088CC;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
/* set default state of collapsible lists as collapsed (hidden) */
|
||||
.nd_collapse-pre-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* for the tagged vlans total when hiding the full list */
|
||||
.nd_vlan-total {
|
||||
float: right;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.nd_side_input {
|
||||
margin-left: -3px;
|
||||
width: 152px;
|
||||
/* little up/down chevron to the right of some collapsed list */
|
||||
.nd_arrow-up-down-right {
|
||||
float: right;
|
||||
margin-top: 1px;
|
||||
margin-right: 1px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.nd_side_select {
|
||||
margin-left: -3px;
|
||||
width: 165px;
|
||||
/* little up arrow to the left of a label for collapsed list */
|
||||
.nd_arrow-up-down-left {
|
||||
float: left;
|
||||
margin-right: 6px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.sidebar .input-prepend {
|
||||
margin-left: -2px;
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* styles for sidebar placement and sizing */
|
||||
|
||||
/* make the sidebar fixed on the screen */
|
||||
.container-fluid > .nd_sidebar {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
width: 200px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
/* nudge content in the sidebar closer to the left */
|
||||
.nd_sidebar-form {
|
||||
padding-left: 0px;
|
||||
margin-top: -9px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* nudge the port name/vlan filter over a little */
|
||||
#nd_port_query {
|
||||
margin-left: 5px !important;
|
||||
width: 152px;
|
||||
/* reduce padding at the bottom of the sidebar content */
|
||||
.container-fluid > .nd_sidebar > .well {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
/* somewhere between span1 and span2 is desirable */
|
||||
#nd_days_select {
|
||||
margin-top: 4px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
/* set the day/mon/year drop-down width */
|
||||
#nd_age_select {
|
||||
margin-top: 4px;
|
||||
width: 95px;
|
||||
}
|
||||
|
||||
/* set the MAC format drop-down width */
|
||||
#nd_mac_format {
|
||||
margin-top: 4px;
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
/* set the MAC format drop-down width */
|
||||
#nd_node_mac_format {
|
||||
margin-left: -2px;
|
||||
margin-top: 4px;
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
/* sidebar submit button width and spacing from Node Props */
|
||||
.sidebar button {
|
||||
margin-top: 9px;
|
||||
margin-left: -3px;
|
||||
width: 165px;
|
||||
}
|
||||
.sidebar #ports_submit {
|
||||
margin-top: 9px;
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
/* little icon inside of search input fields */
|
||||
.field_clear_icon, .field_copy_icon {
|
||||
position: absolute;
|
||||
margin-left: 140px;
|
||||
margin-top: 5px;
|
||||
z-index: 1;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.field_copy_icon {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.field_clear_icon {
|
||||
background-color: #A9DBA9;
|
||||
color: #3A87AD;
|
||||
}
|
||||
|
||||
/* for the ports form, but the positioning is slightly different */
|
||||
#ports_form .field_clear_icon {
|
||||
margin-left: 149px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* change highlighting for form fields which are being used in a search */
|
||||
form .clearfix.success select {
|
||||
background-color: #A9DBA9;
|
||||
}
|
||||
form .clearfix.success input {
|
||||
background-color: #A9DBA9;
|
||||
}
|
||||
|
||||
/* when we use font-awesome icons, override the size */
|
||||
#nd_legend i {
|
||||
width: 9px;
|
||||
}
|
||||
.table-bordered i {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
/* bring sidebar items closer together */
|
||||
.inputs-list label {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.inputs-list i {
|
||||
margin-right: 5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* nudge content closer to the header labels in the sidebar */
|
||||
.inputs-list li:first-child {
|
||||
padding-top: 3px !important;
|
||||
/* pull tab content away from the sidebar */
|
||||
.container-fluid > .content {
|
||||
margin-right: 215px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* sidebar collapser */
|
||||
/* styles for sidebar position controls (collapse, pin) */
|
||||
|
||||
.nd_sidebar_title {
|
||||
margin-left: 10px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.sidebar_pinned {
|
||||
.nd_sidebar-pinned {
|
||||
position: fixed !important;
|
||||
}
|
||||
|
||||
.sidebar_pin_clicked {
|
||||
.nd_sidebar-pin-clicked {
|
||||
color: rgba(255,0,0,0.8) !important;
|
||||
}
|
||||
|
||||
/* for placing the sidebar pin icons */
|
||||
.sidebar_pin {
|
||||
.nd_sidebar-pin {
|
||||
float: left;
|
||||
margin-top: 6px;
|
||||
margin-left: -16px;
|
||||
@@ -359,8 +273,7 @@ form .clearfix.success input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* for placing the sidebar toggle icons */
|
||||
#sidebar_toggle_img_in {
|
||||
#nd_sidebar-toggle-img-in {
|
||||
float: left;
|
||||
margin-top: -9px;
|
||||
margin-left: -16px;
|
||||
@@ -369,8 +282,7 @@ form .clearfix.success input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* for placing the sidebar toggle icons */
|
||||
#sidebar_toggle_img_out {
|
||||
#nd_sidebar-toggle-img-out {
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 7px;
|
||||
@@ -381,7 +293,7 @@ form .clearfix.success input {
|
||||
}
|
||||
|
||||
/* question mark image with popover for netmap instructions */
|
||||
#netmap_help_img {
|
||||
#nd_netmap-help {
|
||||
position: fixed;
|
||||
top: 160px;
|
||||
right: 7px;
|
||||
@@ -393,34 +305,147 @@ form .clearfix.success input {
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* sidebar placement and sizing */
|
||||
/* style customization for many items which appear in the sidebar */
|
||||
|
||||
/* make the sidebar fixed on the screen */
|
||||
.container-fluid > .sidebar {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
width: 200px;
|
||||
left: auto;
|
||||
.nd_sidebar-title {
|
||||
margin-left: 10px;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* smaller padding below form button in sidebar well */
|
||||
.container-fluid > .sidebar > .well {
|
||||
padding-bottom: 15px;
|
||||
/* fixup for prepended checkbox in sidebar */
|
||||
.nd_searchcheckbox {
|
||||
width: 123px;
|
||||
padding-left: 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* make the content start more to the left now the sidebar is narrower */
|
||||
.container-fluid > .content {
|
||||
margin-right: 215px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
/* nudge content in the sidebar closer to the left */
|
||||
.nd_sidesearchform {
|
||||
padding-left: 0px;
|
||||
margin-top: -9px;
|
||||
/* fixup for prepended checkbox in sidebar */
|
||||
.nd_sidebar .input-prepend {
|
||||
margin-left: -2px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
/* for some reason bootstrap 2.1.0 displays add-on as block - no check supprt? */
|
||||
.nd_checkboxlabel {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* fixup for placing the Archived "A" inside the prepended checkbox */
|
||||
.nd_legendlabel {
|
||||
float: right;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* placement of form field in sidebar */
|
||||
.nd_side-input {
|
||||
margin-left: -3px;
|
||||
width: 152px;
|
||||
}
|
||||
|
||||
/* placement of form field in sidebar */
|
||||
.nd_side-select {
|
||||
margin-left: -3px;
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
/* nudge the port name/vlan filter over a little (as compared to nd_side-select) */
|
||||
#nd_port-query {
|
||||
margin-left: 5px !important;
|
||||
width: 152px;
|
||||
}
|
||||
|
||||
/* set the day/mon/year drop-down width */
|
||||
#nd_days-select {
|
||||
margin-top: 4px;
|
||||
width: 56px;
|
||||
}
|
||||
|
||||
/* set the day/mon/year drop-down width */
|
||||
#nd_age-select {
|
||||
margin-top: 4px;
|
||||
width: 95px;
|
||||
}
|
||||
|
||||
/* set the MAC format drop-down width */
|
||||
#nd_mac-format {
|
||||
margin-top: 4px;
|
||||
width: 154px;
|
||||
}
|
||||
|
||||
/* set the MAC format drop-down width */
|
||||
#nd_node-mac-format {
|
||||
margin-left: -2px;
|
||||
margin-top: 4px;
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
/* sidebar submit button width and spacing */
|
||||
.nd_sidebar button {
|
||||
margin-top: 9px;
|
||||
margin-left: -3px;
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
/* little icon inside of search input fields */
|
||||
.nd_field-clear-icon, .nd_field-copy-icon {
|
||||
position: absolute;
|
||||
margin-left: 140px;
|
||||
margin-top: 5px;
|
||||
z-index: 1;
|
||||
padding: 0px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* little icon inside of search input fields */
|
||||
.nd_field-copy-icon {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* little icon inside of search input fields */
|
||||
.nd_field-clear-icon {
|
||||
background-color: #A9DBA9;
|
||||
color: #3A87AD;
|
||||
}
|
||||
|
||||
/* same for the ports form, but the positioning is slightly different */
|
||||
#ports_form .nd_field-clear-icon {
|
||||
margin-left: 149px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* change bg color for form fields which are being used in a search */
|
||||
form .clearfix.success select {
|
||||
background-color: #A9DBA9;
|
||||
}
|
||||
form .clearfix.success input {
|
||||
background-color: #A9DBA9;
|
||||
}
|
||||
|
||||
/* when we use font-awesome icons, override the size */
|
||||
#nd_legend i {
|
||||
width: 9px;
|
||||
}
|
||||
.table i {
|
||||
width: 9px;
|
||||
}
|
||||
|
||||
/* bring sidebar items closer together */
|
||||
.nd_inputs-list label {
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
/* compact icons for the sidebar legend */
|
||||
.nd_inputs-list i {
|
||||
margin-right: 5px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
/* nudge content closer to the header labels in the sidebar */
|
||||
.nd_inputs-list li:first-child {
|
||||
padding-top: 3px !important;
|
||||
}
|
||||
|
||||
/* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ */
|
||||
/* D3 SVG */
|
||||
|
||||
|
||||
BIN
Netdisco/share/public/css/smoothness/images/animated-overlay.gif
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 212 B |
|
After Width: | Height: | Size: 208 B |
|
After Width: | Height: | Size: 335 B |
|
After Width: | Height: | Size: 207 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 262 B |
|
After Width: | Height: | Size: 332 B |
|
After Width: | Height: | Size: 280 B |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
5
Netdisco/share/public/css/smoothness/jquery-ui.custom.min.css
vendored
Normal file
6
Netdisco/share/public/javascripts/jquery-ui.custom.min.js
vendored
Normal file
7
Netdisco/share/public/javascripts/jquery.qtip.min.js
vendored
Normal file
@@ -9,8 +9,8 @@ function do_search (event, tab) {
|
||||
|
||||
// page title
|
||||
var pgtitle = 'Netdisco';
|
||||
if ($('#nd_device_name').text().length) {
|
||||
var pgtitle = $('#nd_device_name').text() +' - '+ $('#'+ tab + '_link').text();
|
||||
if ($('#nd_device-name').text().length) {
|
||||
var pgtitle = $('#nd_device-name').text() +' - '+ $('#'+ tab + '_link').text();
|
||||
}
|
||||
|
||||
// each sidebar search form has a hidden copy of the main navbar search
|
||||
@@ -26,16 +26,16 @@ function do_search (event, tab) {
|
||||
// hide or show sidebars depending on previous state,
|
||||
// and whether the sidebar contains any content (detected by TT)
|
||||
if (has_sidebar[tab] == 0) {
|
||||
$('.sidebar, #sidebar_toggle_img_out').hide();
|
||||
$('.nd_sidebar, #nd_sidebar-toggle-img-out').hide();
|
||||
$('.content').css('margin-right', '10px');
|
||||
}
|
||||
else {
|
||||
if (sidebar_hidden) {
|
||||
$('#sidebar_toggle_img_out').show();
|
||||
$('#nd_sidebar-toggle-img-out').show();
|
||||
}
|
||||
else {
|
||||
$('.content').css('margin-right', '215px');
|
||||
$('.sidebar').show();
|
||||
$('.nd_sidebar').show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ function do_search (event, tab) {
|
||||
|
||||
// update browser search history with the new query.
|
||||
// however if it's the same tab, this is a *replace* of the query url.
|
||||
// and just skip this bit if it's the reports display.
|
||||
if (path != 'report' && window.History && window.History.enabled) {
|
||||
// and just skip this bit if it's the report or admin display.
|
||||
if (path != 'report' && path != 'admin' && window.History && window.History.enabled) {
|
||||
is_from_history_plugin = 1;
|
||||
window.History.replaceState(
|
||||
{name: tab, fields: $(form).serializeArray()},
|
||||
@@ -104,8 +104,8 @@ function update_content(from, to) {
|
||||
|
||||
// page title
|
||||
var pgtitle = 'Netdisco';
|
||||
if ($('#nd_device_name').text().length) {
|
||||
var pgtitle = $('#nd_device_name').text() +' - '+ $('#'+ to + '_link').text();
|
||||
if ($('#nd_device-name').text().length) {
|
||||
var pgtitle = $('#nd_device-name').text() +' - '+ $('#'+ to + '_link').text();
|
||||
}
|
||||
|
||||
if (window.History && window.History.enabled && is_from_state_event == 0) {
|
||||
@@ -144,7 +144,7 @@ function device_form_state(e) {
|
||||
$('#nq').css('text-decoration', 'line-through');
|
||||
|
||||
if (e.attr('type') == 'text') {
|
||||
$('.field_copy_icon').hide();
|
||||
$('.nd_field-copy-icon').hide();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,20 +160,20 @@ function device_form_state(e) {
|
||||
function(n,i) {return($(n).val() != "")}).length;
|
||||
if (num_empty === 3) {
|
||||
$('#nq').css('text-decoration', 'none');
|
||||
$('.field_copy_icon').show();
|
||||
$('.nd_field-copy-icon').show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
// sidebar form fields should change colour and have bin/copy icon
|
||||
$('.field_copy_icon').hide();
|
||||
$('.field_clear_icon').hide();
|
||||
$('.nd_field-copy-icon').hide();
|
||||
$('.nd_field-clear-icon').hide();
|
||||
|
||||
// activate typeahead on the main search box, for device names only
|
||||
$('#nq').typeahead({
|
||||
source: function (query, process) {
|
||||
return $.get('/ajax/data/device/typeahead', { query: query }, function (data) {
|
||||
return $.get('/ajax/data/devicename/typeahead', { query: query }, function (data) {
|
||||
return process(data);
|
||||
});
|
||||
}
|
||||
@@ -197,30 +197,30 @@ $(document).ready(function() {
|
||||
$('.add-on :checkbox').each(syncCheckBox).click(syncCheckBox);
|
||||
|
||||
// sidebar toggle - pinning
|
||||
$('.sidebar_pin').click(function() {
|
||||
$('.sidebar').toggleClass('sidebar_pinned');
|
||||
$('.sidebar_pin').toggleClass('sidebar_pin_clicked');
|
||||
$('.nd_sidebar-pin').click(function() {
|
||||
$('.nd_sidebar').toggleClass('nd_sidebar-pinned');
|
||||
$('.nd_sidebar-pin').toggleClass('nd_sidebar-pin-clicked');
|
||||
// update tooltip note for current state
|
||||
if ($('.sidebar_pin').hasClass('sidebar_pin_clicked')) {
|
||||
$('.sidebar_pin').first().data('tooltip').options.title = 'Unpin Sidebar';
|
||||
if ($('.nd_sidebar-pin').hasClass('nd_sidebar-pin-clicked')) {
|
||||
$('.nd_sidebar-pin').first().data('tooltip').options.title = 'Unpin Sidebar';
|
||||
}
|
||||
else {
|
||||
$('.sidebar_pin').first().data('tooltip').options.title = 'Pin Sidebar';
|
||||
$('.nd_sidebar-pin').first().data('tooltip').options.title = 'Pin Sidebar';
|
||||
}
|
||||
});
|
||||
|
||||
// sidebar toggle - trigger in/out on image click()
|
||||
$('#sidebar_toggle_img_in').click(function() {
|
||||
$('.sidebar').toggle(250);
|
||||
$('#sidebar_toggle_img_out').toggle();
|
||||
$('#nd_sidebar-toggle-img-in').click(function() {
|
||||
$('.nd_sidebar').toggle(250);
|
||||
$('#nd_sidebar-toggle-img-out').toggle();
|
||||
$('.content').css('margin-right', '10px');
|
||||
sidebar_hidden = 1;
|
||||
});
|
||||
$('#sidebar_toggle_img_out').click(function() {
|
||||
$('#sidebar_toggle_img_out').toggle();
|
||||
$('#nd_sidebar-toggle-img-out').click(function() {
|
||||
$('#nd_sidebar-toggle-img-out').toggle();
|
||||
$('.content').css('margin-right', '215px');
|
||||
$('.sidebar').toggle(250);
|
||||
if (! $('.sidebar').hasClass('sidebar_pinned')) {
|
||||
$('.nd_sidebar').toggle(250);
|
||||
if (! $('.nd_sidebar').hasClass('nd_sidebar-pinned')) {
|
||||
$(window).scrollTop(0);
|
||||
}
|
||||
sidebar_hidden = 0;
|
||||
@@ -228,7 +228,7 @@ $(document).ready(function() {
|
||||
|
||||
// could not get twitter bootstrap tabs to behave, so implemented this
|
||||
// but warning! will probably not work for dropdowns in tabs
|
||||
$('#search_results li').delegate('a', 'click', function(event) {
|
||||
$('#nd_search-results li').delegate('a', 'click', function(event) {
|
||||
event.preventDefault();
|
||||
var from_li = $('.nav-tabs').find('> .active').first();
|
||||
var to_li = $(this).parent('li')
|
||||
|
||||
@@ -32,12 +32,12 @@ function port_control (e) {
|
||||
}
|
||||
else if ($.trim(td.attr('data-action')) == 'false') {
|
||||
$(e).next('span').text('');
|
||||
$(e).toggleClass('nd_power_on');
|
||||
$(e).toggleClass('nd_power-on');
|
||||
$(e).data('tooltip').options.title = 'Click to Enable';
|
||||
td.attr('data-action', 'true');
|
||||
}
|
||||
else if ($.trim(td.attr('data-action')) == 'true') {
|
||||
$(e).toggleClass('nd_power_on');
|
||||
$(e).toggleClass('nd_power-on');
|
||||
$(e).data('tooltip').options.title = 'Click to Disable';
|
||||
td.attr('data-action', 'false');
|
||||
}
|
||||
|
||||
42
Netdisco/share/views/admintask.tt
Normal file
@@ -0,0 +1,42 @@
|
||||
<i class="nd_sidebar-toggle icon-wrench icon-large" id="nd_sidebar-toggle-img-out"
|
||||
rel="tooltip" data-placement="left" data-offset="5" data-title="Show Sidebar"></i>
|
||||
<div class="container-fluid">
|
||||
<div class="nd_sidebar nd_sidebar-pinned">
|
||||
<div class="well">
|
||||
<i class="nd_sidebar-toggle icon-signout" id="nd_sidebar-toggle-img-in"
|
||||
rel="tooltip" data-placement="left" data-offset="5" data-title="Hide Sidebar"></i>
|
||||
<i class="nd_sidebar-pin icon-pushpin nd_sidebar-pin-clicked"
|
||||
rel="tooltip" data-placement="left" data-offset="5" data-title="Unpin Sidebar"></i>
|
||||
|
||||
<div class="tab-content">
|
||||
<div id="[% task.tag %]_search" class="tab-pane active">
|
||||
<form id="[% task.tag %]_form" class="nd_sidebar-form form-stacked"
|
||||
method="get" action="[% uri_for('/admin') %]">
|
||||
[% TRY %]
|
||||
[% INCLUDE "sidebar/admintask/${task.tag}.tt" %]
|
||||
<script type="text/javascript">has_sidebar["[% task.tag %]"] = 1;</script>
|
||||
[% CATCH %]
|
||||
<script type="text/javascript">has_sidebar["[% task.tag %]"] = 0;</script>
|
||||
[% END %]
|
||||
</form>
|
||||
</div> <!-- /tab-pane -->
|
||||
</div> <!-- /tab-content -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<ul id="nd_search-results" class="nav nav-tabs">
|
||||
<li class="active"><a id="[% task.tag %]_link" class="nd_single-tab"
|
||||
href="#[% task.tag %]_pane">[% task.label %]</a></li>
|
||||
[% IF task.tag == 'jobqueue' %]
|
||||
<span id="nd_device-name"></span>
|
||||
[% END %]
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="[% task.tag %]_pane"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
[%+ INCLUDE 'js/admintask.js' -%]
|
||||
</script>
|
||||
51
Netdisco/share/views/ajax/admintask/jobqueue.tt
Normal file
@@ -0,0 +1,51 @@
|
||||
<table class="table table-bordered table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="nd_center-cell">Entered</th>
|
||||
<th class="nd_center-cell">Action</th>
|
||||
<th class="nd_center-cell">Status</th>
|
||||
<th class="nd_center-cell">Device</th>
|
||||
<th class="nd_center-cell">Port</th>
|
||||
<th class="nd_center-cell">Param</th>
|
||||
<th class="nd_center-cell">User</th>
|
||||
<th class="nd_center-cell">Started</th>
|
||||
<th class="nd_center-cell">Finished</th>
|
||||
<th class="nd_center-cell">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</tbody>
|
||||
[% WHILE (row = results.next) %]
|
||||
<tr
|
||||
[% ' class="success"' IF row.status == 'done' %]
|
||||
[% ' class="error"' IF row.status == 'error' %]
|
||||
[% ' class="info"' IF row.status.search('^queued-') %]
|
||||
>
|
||||
<td class="nd_center-cell">[% row.entered_stamp | html_entity %]</td>
|
||||
<td class="nd_center-cell">
|
||||
[% FOREACH word IN row.action.split('_') %]
|
||||
[% word.ucfirst | html_entity %]
|
||||
[% END %]
|
||||
</td>
|
||||
[% IF row.status.search('^queued-') %]
|
||||
<td class="nd_center-cell">Running on "[% row.status.remove('^queued-') | html_entity %]"</td>
|
||||
[% ELSE %]
|
||||
<td class="nd_center-cell">[% row.status.ucfirst | html_entity %]</td>
|
||||
[% END %]
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% uri_for('/device') %]?q=[% row.device | uri %]">[% row.device | html_entity %]</a></td>
|
||||
<td class="nd_center-cell">[% row.port | html_entity %]</td>
|
||||
<td class="nd_center-cell">[% row.subaction | html_entity %]</td>
|
||||
<td class="nd_center-cell">[% row.username | html_entity %]</td>
|
||||
<td class="nd_center-cell">[% row.started_stamp | html_entity %]</td>
|
||||
<td class="nd_center-cell">[% row.finished_stamp | html_entity %]</td>
|
||||
<td class="nd_center-cell">
|
||||
<form name="del" class="nd_inline-form">
|
||||
<input name="job" type="hidden" value="[% row.job | html_entity %]">
|
||||
<button class="btn" name="del" type="submit"><i class="icon-trash text-error"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
44
Netdisco/share/views/ajax/admintask/pseudodevice.tt
Normal file
@@ -0,0 +1,44 @@
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="nd_center-cell">Device Name</th>
|
||||
<th class="nd_center-cell">Device IP</th>
|
||||
<th class="nd_center-cell">Number of Ports</th>
|
||||
<th class="nd_center-cell">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</tbody>
|
||||
<tr>
|
||||
<form name="add">
|
||||
<td class="nd_center-cell"><input name="dns" type="text"></td>
|
||||
<td class="nd_center-cell"><input name="ip" type="text"></td>
|
||||
<td class="nd_center-cell"><input name="ports" type="number"></td>
|
||||
<td class="nd_center-cell">
|
||||
<button class="btn btn-small" name="add" type="submit"><i class="icon-plus-sign"></i> Add</button>
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
[% WHILE (row = results.next) %]
|
||||
<tr>
|
||||
<form name="update">
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% uri_for('/device') %]?q=[% row.dns | uri %]">[% row.dns | html_entity %]</a></td>
|
||||
<td class="nd_center-cell">[% row.ip | html_entity %]</td>
|
||||
<td class="nd_center-cell"><input name="ports" type="number" value="[% row.port_count | html_entity %]"></td>
|
||||
<td class="nd_center-cell">
|
||||
<input name="dns" type="hidden" value="[% row.dns | html_entity %]">
|
||||
<input name="ip" type="hidden" value="[% row.ip | html_entity %]">
|
||||
<button class="btn" name="update" type="submit"><i class="icon-save text-warning"></i></button>
|
||||
</form>
|
||||
<form name="del" class="nd_inline-form">
|
||||
<input name="dns" type="hidden" value="[% row.dns | html_entity %]">
|
||||
<input name="ip" type="hidden" value="[% row.ip | html_entity %]">
|
||||
<input name="ports" type="hidden" value="[% row.port_count | html_entity %]">
|
||||
<button class="btn" name="del" type="submit"><i class="icon-trash text-error"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
64
Netdisco/share/views/ajax/admintask/topology.tt
Normal file
@@ -0,0 +1,64 @@
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="nd_center-cell">Left Device</th>
|
||||
<th class="nd_center-cell">Left Port</th>
|
||||
<th class="nd_center-cell">Right Device</th>
|
||||
<th class="nd_center-cell">Right Port</th>
|
||||
<th class="nd_center-cell">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</tbody>
|
||||
<tr>
|
||||
<form name="add">
|
||||
<td class="nd_center-cell">
|
||||
<div class="input-append">
|
||||
<input class="nd_topo_dev nd_topo_dev1" name="dev1" type="text">
|
||||
<span class="add-on nd_topo_dev_caret"><i class="icon-caret-down icon-large"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<div class="input-append">
|
||||
<input class="nd_topo_port nd_topo_dev1" name="port1" type="text">
|
||||
<span class="add-on nd_topo_port_caret"><i class="icon-caret-down icon-large"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<div class="input-append">
|
||||
<input class="nd_topo_dev nd_topo_dev2" name="dev2" type="text">
|
||||
<span class="add-on nd_topo_dev_caret"><i class="icon-caret-down icon-large"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<div class="input-append">
|
||||
<input class="nd_topo_port nd_topo_dev2" name="port2" type="text">
|
||||
<span class="add-on nd_topo_port_caret"><i class="icon-caret-down icon-large"></i></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="nd_center-cell">
|
||||
<button class="btn btn-small" name="add" type="submit"><i class="icon-plus-sign"></i> Add</button>
|
||||
</td>
|
||||
</form>
|
||||
</tr>
|
||||
[% WHILE (row = results.next) %]
|
||||
<tr>
|
||||
<form name="del">
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% uri_for('/device') %]?q=[% row.dev1 | uri %]">[% row.dev1 | html_entity %]</a></td>
|
||||
<td class="nd_center-cell">[% row.port1 | html_entity %]</td>
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% uri_for('/device') %]?q=[% row.dev2 | uri %]">[% row.dev2 | html_entity %]</a></td>
|
||||
<td class="nd_center-cell">[% row.port2 | html_entity %]</td>
|
||||
<td class="nd_center-cell">
|
||||
<input name="dev1" type="hidden" value="[% row.dev1 | html_entity %]">
|
||||
<input name="port1" type="hidden" value="[% row.port1 | html_entity %]">
|
||||
<input name="dev2" type="hidden" value="[% row.dev2 | html_entity %]">
|
||||
<input name="port2" type="hidden" value="[% row.port2 | html_entity %]">
|
||||
<button class="btn" name="del" type="submit"><i class="icon-trash text-error"></i></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Address</th>
|
||||
<th>DNS</th>
|
||||
<th class="center_cell">Interface</th>
|
||||
<th class="nd_center-cell">Interface</th>
|
||||
<th>Description</th>
|
||||
<th>Prefix</th>
|
||||
</tr>
|
||||
@@ -13,7 +13,7 @@
|
||||
<tr>
|
||||
<td>[% row.alias | html_entity %]</a>
|
||||
<td>[% row.dns | html_entity %]</a>
|
||||
<td class="center_cell"><a class="nd_linkcell"
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% device_ports %]&q=[% params.q | uri %]&f=[% row.port | uri %]">[% row.port | html_entity %]</a></td>
|
||||
<td>[% row.device_port.name | html_entity %]</td>
|
||||
<td><a class="nd_linkcell"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-condensed table-striped">
|
||||
<table class="table table-condensed table-striped">
|
||||
</tbody>
|
||||
<tr>
|
||||
<td>System Name</td>
|
||||
@@ -7,11 +7,11 @@
|
||||
<tr>
|
||||
<td>Location
|
||||
[% IF vars.user.port_control %]
|
||||
<i class="icon-edit nd_edit_icon nd_device_details_edit"></i>
|
||||
<i class="icon-edit nd_edit-icon nd_device-details-edit"></i>
|
||||
[% END %]
|
||||
</td>
|
||||
[% IF vars.user.port_control %]
|
||||
<td class="nd_editable_cell" contenteditable="true"
|
||||
<td class="nd_editable-cell" contenteditable="true"
|
||||
data-field="location" data-for-device="[% d.ip %]">
|
||||
[% d.location | html_entity %]
|
||||
</td>
|
||||
@@ -25,11 +25,11 @@
|
||||
<tr>
|
||||
<td>Contact
|
||||
[% IF vars.user.port_control %]
|
||||
<i class="icon-edit nd_edit_icon nd_device_details_edit"></i>
|
||||
<i class="icon-edit nd_edit-icon nd_device-details-edit"></i>
|
||||
[% END %]
|
||||
</td>
|
||||
[% IF vars.user.port_control %]
|
||||
<td class="nd_editable_cell" contenteditable="true"
|
||||
<td class="nd_editable-cell" contenteditable="true"
|
||||
data-field="contact" data-for-device="[% d.ip | html_entity %]">
|
||||
[% d.contact | html_entity %]
|
||||
</td>
|
||||
@@ -93,5 +93,24 @@
|
||||
<td>VTP Domain</td>
|
||||
<td>[% d.vtp_domain | html_entity %]</td>
|
||||
</tr>
|
||||
[% IF vars.user.admin %]
|
||||
<tr>
|
||||
<td>Admin Tasks</td>
|
||||
<td>
|
||||
<form method="post" class="nd_inline-form" action="[% uri_for('/admin/discover') %]">
|
||||
<input type="hidden" value="[% d.ip %]" name="device" type="text"/>
|
||||
<button type="submit" class="btn btn-info btn-small">Discover</button>
|
||||
</form>
|
||||
<form method="post" class="nd_inline-form" action="[% uri_for('/admin/arpnip') %]">
|
||||
<input type="hidden" value="[% d.ip %]" name="device" type="text"/>
|
||||
<button type="submit" class="btn btn-info btn-small">Arpnip</button>
|
||||
</form>
|
||||
<form method="post" class="nd_inline-form" action="[% uri_for('/admin/macsuck') %]">
|
||||
<input type="hidden" value="[% d.ip %]" name="device" type="text"/>
|
||||
<button type="submit" class="btn btn-info btn-small">Macsuck</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -6,7 +6,10 @@
|
||||
[% NEXT IF item.name == 'c_admin' %]
|
||||
[% NEXT IF item.name == 'c_nodes' AND params.c_nodes AND params.c_neighbors %]
|
||||
[% NEXT UNLESS params.${item.name} %]
|
||||
<th[% ' class="center_cell"' IF NOT loop.first %]>[% item.label | html_entity %]</th>
|
||||
<th[% ' class="nd_nudge-for-icon"' IF
|
||||
(vars.user.port_control AND params.c_admin AND (item.name == 'c_port' OR item.name == 'c_name')) %]>
|
||||
[% item.label | html_entity %]
|
||||
</th>
|
||||
[% END %]
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -25,87 +28,109 @@
|
||||
[% END %]
|
||||
</td>
|
||||
|
||||
[% FOREACH config IN settings._extra_device_port_cols %]
|
||||
[% NEXT UNLESS config.position == 'left' AND params.${config.name} %]
|
||||
<td>
|
||||
[% TRY %]
|
||||
[% INCLUDE "plugin/${config.name}/device_port_column.tt" %]
|
||||
[% CATCH %]
|
||||
<!-- dummy content required by Template Toolkit TRY -->
|
||||
[% END %]
|
||||
</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_port %]
|
||||
[% IF vars.user.port_control AND params.c_admin %]
|
||||
[% IF row.up_admin == 'up' %]
|
||||
<td nowrap class="nd_editable_cell" data-action="down"
|
||||
data-field="c_port" data-for-device="[% device | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-hand-down nd_hand_icon"
|
||||
<td nowrap class="nd_editable-cell" data-action="down"
|
||||
data-field="c_port" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-hand-down nd_hand-icon"
|
||||
rel="tooltip" data-placement="top" data-offset="3"
|
||||
data-animation="" data-title="Click to Disable"></i>
|
||||
[% ELSE %]
|
||||
<td nowrap class="nd_editable_cell" data-action="up"
|
||||
data-field="c_port" data-for-device="[% device | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-hand-up nd_hand_icon"
|
||||
<td nowrap class="nd_editable-cell" data-action="up"
|
||||
data-field="c_port" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-hand-up nd_hand-icon"
|
||||
rel="tooltip" data-placement="top" data-offset="3"
|
||||
data-animation="" data-title="Click to Enable"></i>
|
||||
[% END %]
|
||||
[% ELSE %]
|
||||
<td nowrap>
|
||||
[% END %]
|
||||
<a class="nd_linkcell nd_this_port_only" href="[% uri_for('/device',
|
||||
<a class="nd_linkcell nd_this-port-only" href="[% uri_for('/device',
|
||||
self_options) %]&q=[% params.q | uri %]&f=[% row.port | uri %]">
|
||||
[% row.port | html_entity %]
|
||||
</a></td>
|
||||
[% END %]
|
||||
|
||||
[% FOREACH config IN settings._extra_device_port_cols %]
|
||||
[% NEXT UNLESS config.position == 'mid' AND params.${config.name} %]
|
||||
<td>
|
||||
[% TRY %]
|
||||
[% INCLUDE "plugin/${config.name}/device_port_column.tt" %]
|
||||
[% CATCH %]
|
||||
<!-- dummy content required by Template Toolkit TRY -->
|
||||
[% END %]
|
||||
</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_descr %]
|
||||
<td nowrap class="center_cell">[% row.descr | html_entity %]</td>
|
||||
<td nowrap>[% row.descr | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_type %]
|
||||
<td class="center_cell">[% row.type | html_entity %]</td>
|
||||
<td>[% row.type | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_duplex %]
|
||||
<td class="center_cell">
|
||||
<td>
|
||||
[% IF row.up == 'up' AND row.duplex %]
|
||||
[% row.duplex_admin | html_entity %] / [% row.duplex | html_entity %]
|
||||
[% row.duplex_admin.ucfirst | html_entity %] / [% row.duplex.ucfirst | html_entity %]
|
||||
[% END %]
|
||||
</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_lastchange %]
|
||||
<td class="center_cell">[% row.lastchange_stamp | html_entity %]</td>
|
||||
<td>[% row.lastchange_stamp | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_name %]
|
||||
[% IF vars.user.port_control AND params.c_admin %]
|
||||
<td nowrap class="center_cell nd_editable_cell" contenteditable="true"
|
||||
data-field="c_name" data-for-device="[% device | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-edit nd_edit_icon"></i>
|
||||
<td nowrap class="nd_editable-cell" contenteditable="true"
|
||||
data-field="c_name" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-edit nd_edit-icon"></i>
|
||||
[% ELSE %]
|
||||
<td nowrap class="center_cell">
|
||||
<td nowrap>
|
||||
[% END %]
|
||||
<div class="nd_editable_cell_content">
|
||||
<div class="nd_editable-cell-content">
|
||||
[% row.name | html_entity %]
|
||||
</div>
|
||||
</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_speed %]
|
||||
<td class="center_cell">[% row.speed | html_entity %]</td>
|
||||
<td>[% row.speed | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_mac %]
|
||||
<td class="center_cell">[% row.mac | html_entity %]</td>
|
||||
<td>[% row.mac | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_mtu %]
|
||||
<td class="center_cell">[% row.mtu | html_entity %]</td>
|
||||
<td>[% row.mtu | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_vlan %]
|
||||
[% IF vars.user.port_control AND params.c_admin %]
|
||||
<td class="center_cell nd_editable_cell" contenteditable="true"
|
||||
data-field="c_vlan" data-for-device="[% device | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-edit nd_edit_icon"></i>
|
||||
<div class="nd_editable_cell_content">
|
||||
<td class="nd_editable-cell" contenteditable="true"
|
||||
data-field="c_vlan" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]">
|
||||
<i class="icon-edit nd_edit-icon"></i>
|
||||
<div class="nd_editable-cell-content">
|
||||
[% IF row.vlan %][% row.vlan | html_entity %][% END %]
|
||||
</div>
|
||||
</td>
|
||||
[% ELSE %]
|
||||
<td class="center_cell">
|
||||
<td>
|
||||
<a class="nd_linkcell"
|
||||
href="[% uri_for('/search') %]?tab=vlan&q=[% row.vlan | uri %]">
|
||||
[% row.vlan | html_entity %]</a>
|
||||
@@ -123,11 +148,10 @@
|
||||
[% SET output = output _ ', ' IF NOT loop.last %]
|
||||
[% END %]
|
||||
[% IF row.tagged_vlans_count > 10 %] [%# TODO make this a settable variable %]
|
||||
[% SET output = '<div class="vlan_total">(' _ row.tagged_vlans_count
|
||||
_ ')</div><span class="nd_linkcell nd_collapse_vlans">
|
||||
<i class="cell-arrow-up-down icon-chevron-up icon-large">
|
||||
</i>Show VLANs</span>
|
||||
<div class="nd_collapsing nd_collapse_pre_hidden">' _ output %]
|
||||
[% SET output = '<div class="nd_vlan-total">(' _ row.tagged_vlans_count
|
||||
_ ')</div><span class="nd_linkcell nd_collapse-vlans">
|
||||
<div class="nd_arrow-up-down-left icon-chevron-up icon-large"></div>Show VLANs</span>
|
||||
<div class="nd_collapsing nd_collapse-pre-hidden">' _ output %]
|
||||
[% SET output = output _ '</div>' %]
|
||||
[% END %]
|
||||
[% output %]
|
||||
@@ -140,15 +164,15 @@
|
||||
[% IF row.power.admin == 'true' %]
|
||||
[% IF vars.user.port_control AND params.c_admin %]
|
||||
<td nowrap data-action="false"
|
||||
data-field="c_power" data-for-device="[% device | html_entity %]"
|
||||
data-field="c_power" data-for-device="[% device.ip | html_entity %]"
|
||||
data-for-port="[% row.port | html_entity %]">
|
||||
|
||||
<i class="icon-off nd_power_icon nd_power_on"
|
||||
<i class="icon-off nd_power-icon nd_power-on"
|
||||
rel="tooltip" data-placement="top" data-offset="3"
|
||||
data-animation="" data-title="Click to Disable"></i>
|
||||
[% ELSE %]
|
||||
<td nowrap>
|
||||
<i class="icon-off nd_power_on"></i>
|
||||
<i class="icon-off nd_power-on"></i>
|
||||
[% END %]
|
||||
<span>
|
||||
[% IF row.power.power > 0 %]
|
||||
@@ -160,10 +184,10 @@
|
||||
[% ELSE %]
|
||||
[% IF vars.user.port_control AND params.c_admin %]
|
||||
<td nowrap data-action="true"
|
||||
data-field="c_power" data-for-device="[% device | html_entity %]"
|
||||
data-field="c_power" data-for-device="[% device.ip | html_entity %]"
|
||||
data-for-port="[% row.port | html_entity %]">
|
||||
|
||||
<i class="icon-off nd_power_icon"
|
||||
<i class="icon-off nd_power-icon"
|
||||
rel="tooltip" data-placement="top" data-offset="3"
|
||||
data-animation="" data-title="Click to Enable"></i>
|
||||
[% ELSE %]
|
||||
@@ -179,22 +203,25 @@
|
||||
|
||||
[% IF params.c_nodes OR params.c_neighbors %]
|
||||
<td>
|
||||
[% IF params.c_neighbors AND row.remote_ip %]
|
||||
[% IF params.c_neighbors AND (row.remote_ip OR row.is_uplink) %]
|
||||
[% IF row.neighbor %]
|
||||
<a href="[% uri_for('/device',
|
||||
self_options) %]&q=[% row.neighbor.dns || row.neighbor.ip | uri %]&f=[% row.remote_port | uri %]">
|
||||
[% row.neighbor.dns.remove(settings.domain_suffix) || row.neighbor.ip | html_entity %]
|
||||
([% row.remote_port | html_entity %])</a>
|
||||
[% ELSE %]
|
||||
[% ELSIF row.remote_ip AND row.remote_port %]
|
||||
<span class="label label-important">N</span>
|
||||
<a href="[% search_node %]&q=[% row.remote_ip | uri %]">
|
||||
[% row.remote_ip | html_entity %] (port: [% row.remote_port | html_entity %]
|
||||
id: [% (row.remote_type _ ' / ') IF row.remote_type %][% row.remote_id | html_entity %])</a>
|
||||
[% ' id: '_ row.remote_type IF row.remote_type%]
|
||||
[% ' type: '_ row.remote_id IF row.remote_id%])</a>
|
||||
[% ELSE %]
|
||||
<span class="label label-important">N</span> (probable neighbor)
|
||||
[% END %]
|
||||
[% END %]
|
||||
[% IF params.c_nodes %]
|
||||
[% FOREACH node IN row.$nodes %]
|
||||
[% '<br/>' IF row.remote_ip OR NOT loop.first %]
|
||||
[% '<br/>' IF (row.remote_ip OR row.is_uplink) OR NOT loop.first %]
|
||||
[% '<span class="label label-warning">A</span> ' IF NOT node.active %]
|
||||
<a href="[% search_node %]&q=[% node.net_mac.$mac_format_call | uri %]">
|
||||
[% node.net_mac.$mac_format_call | html_entity %]</a>
|
||||
@@ -216,12 +243,23 @@
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_stp %]
|
||||
<td class="center_cell">[% row.stp | html_entity %]</td>
|
||||
<td>[% row.stp | html_entity %]</td>
|
||||
[% END %]
|
||||
|
||||
[% IF params.c_up %]
|
||||
<td class="center_cell">
|
||||
[% row.up_admin | html_entity %] / [% row.up | html_entity %]
|
||||
<td>
|
||||
[% row.up_admin.ucfirst | html_entity %] / [% row.up.ucfirst | html_entity %]
|
||||
</td>
|
||||
[% END %]
|
||||
|
||||
[% FOREACH config IN settings._extra_device_port_cols %]
|
||||
[% NEXT UNLESS config.position == 'right' AND params.${config.name} %]
|
||||
<td>
|
||||
[% TRY %]
|
||||
[% INCLUDE "plugin/${config.name}/device_port_column.tt" %]
|
||||
[% CATCH %]
|
||||
<!-- dummy content required by Template Toolkit TRY -->
|
||||
[% END %]
|
||||
</td>
|
||||
[% END %]
|
||||
</tr>
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="center_cell">Left Device</th>
|
||||
<th class="center_cell">Interface</th>
|
||||
<th class="center_cell">Duplex</th>
|
||||
<th class="center_cell">Right Device</th>
|
||||
<th class="center_cell">Interface</th>
|
||||
<th class="center_cell">Duplex</th>
|
||||
<th class="nd_center-cell">Left Device</th>
|
||||
<th class="nd_center-cell">Interface</th>
|
||||
<th class="nd_center-cell">Duplex</th>
|
||||
<th class="nd_center-cell">Right Device</th>
|
||||
<th class="nd_center-cell">Interface</th>
|
||||
<th class="nd_center-cell">Duplex</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</tbody>
|
||||
[% WHILE (row = results.next) %]
|
||||
<tr>
|
||||
<td class="center_cell">[% row.left_dns || row.left_ip | html_entity %]</a>
|
||||
<td class="center_cell"><a class="nd_linkcell"
|
||||
<td class="nd_center-cell">[% row.left_dns || row.left_ip | html_entity %]</a>
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% device_ports %]&q=[% row.left_dns || row.left_ip | uri %]&f=[% row.left_port | uri %]&c_duplex=on">
|
||||
[% row.left_port | html_entity %]</a></td>
|
||||
<td class="center_cell">[% row.left_duplex.ucfirst | html_entity %]</td>
|
||||
<td class="nd_center-cell">[% row.left_duplex.ucfirst | html_entity %]</td>
|
||||
|
||||
<td class="center_cell">[% row.right_dns || row.right_ip | html_entity %]</a>
|
||||
<td class="center_cell"><a class="nd_linkcell"
|
||||
<td class="nd_center-cell">[% row.right_dns || row.right_ip | html_entity %]</a>
|
||||
<td class="nd_center-cell"><a class="nd_linkcell"
|
||||
href="[% device_ports %]&q=[% row.right_dns || row.right_ip | uri %]&f=[% row.right_port | uri %]&c_duplex=on">
|
||||
[% row.right_port | html_entity %]</a></td>
|
||||
<td class="center_cell">[% row.right_duplex.ucfirst | html_entity %]</td>
|
||||
<td class="nd_center-cell">[% row.right_duplex.ucfirst | html_entity %]</td>
|
||||
</tr>
|
||||
[% END %]
|
||||
</tbody>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Device</th>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC</th>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC</th>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<table class="table-bordered table-condensed table-striped">
|
||||
<table class="table table-bordered table-condensed table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vlan</th>
|
||||
@@ -12,17 +12,17 @@
|
||||
</tbody>
|
||||
[% WHILE (row = results.next) %]
|
||||
<tr>
|
||||
<td><a class="nd_linkcell nd_stealthlink"
|
||||
<td><a class="nd_linkcell nd_stealth-link"
|
||||
href="[% device_ports %]&q=[% row.dns || row.ip | uri %]&f=[% row.vlan.vlan | uri %]">[% row.vlan.vlan | html_entity %]</a></td>
|
||||
<td><a class="nd_linkcell"
|
||||
href="[% device_ports %]&q=[% row.dns || row.ip | uri %]&f=[% row.vlan.vlan | uri %]">[% row.dns || row.ip | html_entity %]</a></td>
|
||||
<td><a class="nd_linkcell nd_stealthlink"
|
||||
<td><a class="nd_linkcell nd_stealth-link"
|
||||
href="[% device_ports %]&q=[% row.dns || row.ip | uri %]&f=[% row.vlan.vlan | uri %]">[% row.vlan.description | html_entity %]</a></td>
|
||||
<td><a class="nd_linkcell nd_stealthlink"
|
||||
<td><a class="nd_linkcell nd_stealth-link"
|
||||
href="[% device_ports %]&q=[% row.dns || row.ip | uri %]&f=[% row.vlan.vlan | uri %]">[% row.model | html_entity %]</a></td>
|
||||
<td><a class="nd_linkcell nd_stealthlink"
|
||||
<td><a class="nd_linkcell nd_stealth-link"
|
||||
href="[% device_ports %]&q=[% row.dns || row.ip | uri %]&f=[% row.vlan.vlan | uri %]">[% row.os | html_entity %]</a></td>
|
||||
<td><a class="nd_linkcell nd_stealthlink"
|
||||
<td><a class="nd_linkcell nd_stealth-link"
|
||||
href="[% device_ports %]&q=[% row.dns || row.ip | uri %]&f=[% row.vlan.vlan | uri %]">[% row.vendor | html_entity %]</a></td>
|
||||
</tr>
|
||||
[% END %]
|
||||
|
||||