Merge of og-work branch, many new features.

Squashed commit of the following:

commit a43c98962a
Author: 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)

commit 09829a25b8
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Jun 3 20:07:31 2013 +0100

    local plugins site_plugins dir

commit b0e804e558
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Jun 3 19:59:04 2013 +0100

    use send_error and redirect from Dancer

commit 3d1185261a
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Jun 3 19:13:40 2013 +0100

    support path config option

commit 31ca119f84
Merge: 9a79855 4d2b3a5
Author: 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 commit 9a79855361, reversing
    changes made to 6fd6118354.

    Conflicts:
    	Netdisco/share/views/plugin/device_port_column/c_observiumsparklines.tt

commit 9a79855361
Merge: 6fd6118 c8c3b82
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Jun 3 00:03:32 2013 +0100

    Merge remote-tracking branch 'origin/master' into og-work

commit 6fd6118354
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun Jun 2 15:47:45 2013 +0100

    extra note about behind proxy

commit 798086ca29
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun Jun 2 15:30:26 2013 +0100

    complete the observium plugin

commit 66b3ced179
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun Jun 2 12:48:06 2013 +0100

    Plugins can have CSS and Javascript loaded within <head>

commit 4d2b3a5307
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 08:50:16 2013 +0100

    get device dns to port template

commit ed1bfa1ae7
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 08:17:02 2013 +0100

    observium sparklines plugin; support X:: namespace

commit 76b7636c74
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 06:30:06 2013 +0100

    rename private settings keys

commit fdac8f6c33
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 05:59:53 2013 +0100

    add macwalk and arpnip buttons to device details

commit 3d688c7d83
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 05:57:20 2013 +0100

    Revert "reduce refresh to 5sec"

    This reverts commit 8ea9ec7dd9.

commit dc62382112
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 05:50:34 2013 +0100

    support for arpwalk and macwalk and all jobs via web

commit 8bc7d83c98
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 30 05:35:41 2013 +0100

    simplify discover options to only discoverall and discover

commit 8ea9ec7dd9
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 20:23:08 2013 +0100

    reduce refresh to 5sec

commit 8c54e6c58b
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 20:11:06 2013 +0100

    show undiscovered neighbor properly

commit e0ee25628f
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 19:54:09 2013 +0100

    avoid unecessary log for queueing

commit d5565423f2
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 19:51:37 2013 +0100

    avoid warning on undefined remote type

commit 5d9b58a6b2
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 19:48:22 2013 +0100

    avoid explosion when not admin

commit 377bb942e0
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 19:46:52 2013 +0100

    avoid undefined warning

commit 08806dcfa2
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 19:46:42 2013 +0100

    get_db_version will be 0 at first deploy

commit 9511c17056
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 29 19:15:55 2013 +0100

    fix name of Template module

commit eb0288de35
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 28 07:17:07 2013 +0100

    initial config settings documentation

commit 7f2ea7f8dc
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 27 15:18:15 2013 +0100

    remove check_mac to own module, use in macsuck too

commit b995cf6398
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 27 15:01:29 2013 +0100

    show probable but undiscovered neighbor is ports display

commit dd8d461188
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 27 14:52:41 2013 +0100

    new schema version for is_uplink and is_uplink_admin

commit 3f6a7b5aa2
Author: 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

commit 33bf9a6599
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 26 19:51:49 2013 +0100

    export store_arp and store_node

commit 0ed356d560
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat May 25 17:12:31 2013 +0100

    use row lock not table lock

commit f830bc3a3b
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat May 25 16:38:33 2013 +0100

    move macsuck/arpnip/discover to ::Core namespace

commit be40788987
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Fri May 24 21:10:34 2013 +0100

    add maybe_uplink to device_port; more macsuck implementation

commit 88371026d5
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Fri May 24 14:34:58 2013 +0100

    start on macsuck; tweak update locking

commit 6f7c87ac07
Author: 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

commit 7c438e01fc
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Fri May 24 12:12:46 2013 +0100

    yet more efficient arpnip

commit c74c56dc02
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Fri May 24 11:34:23 2013 +0100

    guard against race with *_or_* DBIC methods

commit d50c54972e
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 20 23:42:41 2013 +0100

    more efficient arpnip

commit 73c8979130
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 19 22:52:15 2013 +0100

    fix confusing name

commit bf78e82411
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 19 22:37:22 2013 +0100

    fix mistake in DBIx::Class schema

commit 6a5af95836
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 19 22:06:27 2013 +0100

    arpnip implementation

commit 594abd3f82
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 16 00:00:50 2013 +0100

    PostgreSQL explicit locking support.

    Squashed commit of the following:

    commit 76e1539102
    Author: Oliver Gorwits <oliver@cpan.org>
    Date:   Wed May 15 23:54:25 2013 +0100

        finished explicit locking module

    commit 369387258b
    Author: Oliver Gorwits <oliver@cpan.org>
    Date:   Tue May 14 23:50:42 2013 +0100

        initial implementation of locking from schema object

commit 55c6d4fe63
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 14 21:05:01 2013 +0100

    add discover button to device details page

commit 11fd8bf964
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 14 20:43:43 2013 +0100

    fix typo and clear port box on autocomplete dropdown

commit a00f9b5c2e
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 14 20:38:54 2013 +0100

    move admin tasks and remove JobControl package

commit 74bc0023df
Author: 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

commit dd6947f38d
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat May 11 16:51:28 2013 +0100

    fix improper use of bootstrap table class

commit cd5b83f71e
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat May 11 15:55:45 2013 +0100

    fix update view icon in sidebar

commit e9349f325d
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat May 11 11:57:19 2013 +0100

    css audit

commit 201470275d
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 9 23:48:05 2013 +0100

    add job queue to standard plugins list

commit a18a3c72a3
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 9 23:37:43 2013 +0100

    fix table headings and improve Action display in Job Queue

commit 70f5da8bb6
Author: 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

commit 2e8ac83173
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 9 21:53:39 2013 +0100

    more js refactoring for report and search

commit 479ac0e55d
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 9 21:50:29 2013 +0100

    refactor js for device tabs

commit 6a17fe5d6c
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Thu May 9 21:05:42 2013 +0100

    fix crazy races with javasacript by using global delegations

commit e94e3cef3b
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Wed May 8 23:06:41 2013 +0100

    remove Try::Tiny from web runtime

commit c746e68b9b
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 7 21:54:11 2013 +0100

    make topo autocomplete more responsive

commit 24c511786f
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 7 21:52:17 2013 +0100

    display name and IP for device typeahead

commit 52ab7d1266
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 7 21:47:05 2013 +0100

    add drop-down control for the topo form fields

commit 5744b6845f
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 7 21:25:30 2013 +0100

    complete the topology editor (add/delete)

commit b510fbe8c5
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Tue May 7 00:59:11 2013 +0100

    add new admin tasks to default plugins list

commit 11d55e0129
Author: 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.

commit bf7a419d08
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 6 22:16:24 2013 +0100

    add a little colour to lone tab titles

commit 9690a31f19
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 6 22:01:13 2013 +0100

    complete Manage Pseudo Devices

commit 024f4d9a83
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 6 00:49:47 2013 +0100

    use bootstrap font colour instead of css

commit f75f1e5cbf
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon May 6 00:45:18 2013 +0100

    add frontend update/del forms, and display port count

commit f0899e16b3
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 23:53:20 2013 +0100

    add frontend pseudo device add form

commit 3271c01931
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 21:45:17 2013 +0100

    complete the code for admin tasks page loading

commit 38f70624f3
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 17:04:30 2013 +0100

    set up file paths consistently in all scripts

commit c761ca839b
Author: 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

commit f468b48049
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 16:20:39 2013 +0100

    Handle whitespace ahead of OUI data

commit 5c8a5754f6
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 16:16:20 2013 +0100

    also set neighbor info when discovering device interfaces

commit acb988b6af
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 15:34:20 2013 +0100

    try to avoid duplicate execution of scheduled jobs

commit c6bcaf66c5
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 14:16:25 2013 +0100

    do not clobber manual topo when discovering neighbors

commit d9a6a1882a
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sun May 5 13:02:45 2013 +0100

    User icon color indicates port_control/admin ability

commit 2cdcb9db7e
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Apr 29 23:34:27 2013 +0100

    add support for admin tasks as plugins

commit 075a770c9a
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Apr 29 22:23:20 2013 +0100

    skip pseudo devices (vendor netdisco)

commit 045c022d42
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Mon Apr 29 21:58:33 2013 +0100

    incorporate manual topo info from the topology db table

commit 09285d42b4
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 18:39:12 2013 +0100

    add unique constraints to topology table

commit 2780b72e49
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 15:38:05 2013 +0100

    muted help text in sidebar

commit 733d4f83fb
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:39:54 2013 +0100

    sorry, testing hook changes

commit 71e366e352
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:34:36 2013 +0100

    sorry, testing hook changes

commit 7f9eaa99f5
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:33:44 2013 +0100

    sorry, testing hook changes

commit 5215fd632d
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:30:07 2013 +0100

    sorry, testing hook changes

commit be817d60c2
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:21:45 2013 +0100

    sorry, testing hook changes

commit 1fd3695358
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:18:57 2013 +0100

    sorry, testing hook changes

commit ac448c4a91
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:13:03 2013 +0100

    sorry, testing hook changes

commit c563b8d9af
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:08:54 2013 +0100

    sorry, testing hook changes

commit 3abcfb01d5
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:06:25 2013 +0100

    sorry, testing hook changes

commit 877a81facf
Author: Oliver Gorwits <oliver@cpan.org>
Date:   Sat Apr 27 14:05:25 2013 +0100

    sorry, testing hook changes
This commit is contained in:
Oliver Gorwits
2013-06-03 20:38:33 +01:00
parent c8c3b82238
commit 4d0e2461f5
119 changed files with 3524 additions and 778 deletions

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

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

View File

@@ -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();

View File

@@ -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

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

View File

@@ -1,4 +1,4 @@
package App::Netdisco::Util::DiscoverAndStore;
package App::Netdisco::Core::Discover;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
@@ -13,13 +13,13 @@ our @EXPORT = ();
our @EXPORT_OK = qw/
store_device store_interfaces store_wireless
store_vlans store_power store_modules
find_neighbors
store_neighbors discover_new_neighbors
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME
App::Netdisco::Util::DiscoverAndStore
App::Netdisco::Core::Discover
=head1 DESCRIPTION
@@ -52,6 +52,7 @@ sub store_device {
my $hostname = hostname_from_ip($device->ip);
$device->dns($hostname) if length $hostname;
my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8');
# build device aliases suitable for DBIC
my @aliases;
@@ -60,7 +61,7 @@ sub store_device {
my $addr = $ip->addr;
next if $addr eq '0.0.0.0';
next if $ip->within(NetAddr::IP::Lite->new('127.0.0.0/8'));
next if $ip->within($localnet);
next if setting('ignore_private_nets') and $ip->is_rfc1918;
my $iid = $ip_index->{$addr};
@@ -105,7 +106,7 @@ sub store_device {
my $gone = $device->device_ips->delete;
debug sprintf ' [%s] device - removed %s aliases',
$device->ip, $gone;
$device->update_or_insert;
$device->update_or_insert(undef, {for => 'update'});
$device->device_ips->populate(\@aliases);
debug sprintf ' [%s] device - added %d new aliases',
$device->ip, scalar @aliases;
@@ -118,7 +119,7 @@ sub _set_canonical_ip {
my $oldip = $device->ip;
my $newip = $snmp->root_ip;
if (length $newip) {
if (defined $newip) {
if ($oldip ne $newip) {
debug sprintf ' [%s] device - changing root IP to alt IP %s',
$oldip, $newip;
@@ -252,7 +253,7 @@ sub store_interfaces {
my $gone = $device->ports->delete;
debug sprintf ' [%s] interfaces - removed %s interfaces',
$device->ip, $gone;
$device->update_or_insert;
$device->update_or_insert(undef, {for => 'update'});
$device->ports->populate(\@interfaces);
debug sprintf ' [%s] interfaces - added %d new interfaces',
$device->ip, scalar @interfaces;
@@ -562,26 +563,34 @@ sub store_modules {
});
}
=head2 find_neighbors( $device, $snmp )
=head2 store_neighbors( $device, $snmp )
returns: C<@to_discover>
Given a Device database object, and a working SNMP connection, discover and
store the device's port neighbors information.
If any neighbor is unknown to Netdisco, a discover job for it will immediately
be queued (modulo configuration file C<discover_no_type> setting).
Entries in the Topology database table will override any discovered device
port relationships.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
A list of discovererd neighbors will be returned as [C<$ip>, C<$type>] tuples.
=cut
sub find_neighbors {
sub store_neighbors {
my ($device, $snmp) = @_;
my @to_discover = ();
# first allow any manually configred topology to be set
_set_manual_topology($device, $snmp);
my $c_ip = $snmp->c_ip;
unless ($snmp->hasCDP or scalar keys %$c_ip) {
debug sprintf ' [%s] neigh - CDP/LLDP not enabled!', $device->ip;
return;
return @to_discover;
}
my $interfaces = $snmp->interfaces;
@@ -642,17 +651,28 @@ sub find_neighbors {
}
}
# IP Phone detection type fixup
if (defined $remote_type and $remote_type =~ m/(mitel.5\d{3})/i) {
$remote_type = 'IP Phone - '. $remote_type
if $remote_type !~ /ip phone/i;
}
else {
$remote_type = '';
}
# hack for devices seeing multiple neighbors on the port
if (ref [] eq ref $remote_ip) {
debug sprintf
' [%s] neigh - port %s has multiple neighbors, setting remote as self',
$device->ip, $port;
foreach my $n (@$remote_ip) {
debug sprintf
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
$device->ip, $n, $remote_type, $port;
_enqueue_discover($n, $remote_type);
if (wantarray) {
foreach my $n (@$remote_ip) {
debug sprintf
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
$device->ip, $n, $remote_type, $port;
push @to_discover, [$n, $remote_type];
}
}
# set self as remote IP to suppress any further work
@@ -660,6 +680,14 @@ sub find_neighbors {
$remote_port = $port;
}
else {
# what we came here to do.... discover the neighbor
if (wantarray) {
debug sprintf
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
$device->ip, $remote_ip, $remote_type, $port;
push @to_discover, [$remote_ip, $remote_type];
}
$remote_port = $c_port->{$entry};
if (defined $remote_port) {
@@ -672,12 +700,7 @@ sub find_neighbors {
}
}
# XXX too custom? IP Phone detection
if (defined $remote_type and $remote_type =~ m/(mitel.5\d{3})/i) {
$remote_type = 'IP Phone - '. $remote_type
if $remote_type !~ /ip phone/i;
}
# if all the data looks sane, update the port row with neighbor info
my $portrow = schema('netdisco')->resultset('DevicePort')
->single({ip => $device->ip, port => $port});
@@ -687,44 +710,120 @@ sub find_neighbors {
next;
}
if ($portrow->manual_topo) {
info sprintf ' [%s] neigh - %s has manually defined topology',
$device->ip, $port;
next;
}
$portrow->update({
remote_ip => $remote_ip,
remote_port => $remote_port,
remote_type => $remote_type,
remote_id => $remote_id,
is_uplink => \"true",
manual_topo => \"false",
});
debug sprintf
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
$device->ip, $remote_ip, $remote_type, $port;
_enqueue_discover($remote_ip, $remote_type);
}
return @to_discover;
}
# only enqueue if device is not already discovered, and
# discover_no_type config permits the discovery
sub _enqueue_discover {
my ($ip, $remote_type) = @_;
# take data from the topology table and update remote_ip and remote_port
# in the devices table. only use root_ips and skip any bad topo entries.
sub _set_manual_topology {
my ($device, $snmp) = @_;
my $device = get_device($ip);
return if $device->in_storage;
schema('netdisco')->txn_do(sub {
# clear manual topology flags
schema('netdisco')->resultset('DevicePort')->update({manual_topo => \'false'});
my $remote_type_match = setting('discover_no_type');
if ($remote_type and $remote_type_match
and $remote_type =~ m/$remote_type_match/) {
debug sprintf ' queue - %s, type [%s] excluded by discover_no_type',
$ip, $remote_type;
return;
}
my $topo_links = schema('netdisco')->resultset('Topology');
debug sprintf ' [%s] neigh - setting manual topology links', $device->ip;
while (my $link = $topo_links->next) {
# could fail for broken topo, but we ignore to try the rest
try {
schema('netdisco')->txn_do(sub {
# only work on root_ips
my $left = get_device($link->dev1);
my $right = get_device($link->dev2);
# skip bad entries
return unless ($left->in_storage and $right->in_storage);
$left->ports
->single({port => $link->port1}, {for => 'update'})
->update({
remote_ip => $right->ip,
remote_port => $link->port2,
remote_type => undef,
remote_id => undef,
is_uplink => \"true",
manual_topo => \"true",
});
$right->ports
->single({port => $link->port2}, {for => 'update'})
->update({
remote_ip => $left->ip,
remote_port => $link->port1,
remote_type => undef,
remote_id => undef,
is_uplink => \"true",
manual_topo => \"true",
});
});
};
}
});
}
=head2 discover_new_neighbors( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's port neighbors information.
Entries in the Topology database table will override any discovered device
port relationships.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
Any discovered neighbor unknown to Netdisco will have a C<discover> job
immediately queued (subject to the filtering by the C<discover_no_type>
setting).
=cut
sub discover_new_neighbors {
my @to_discover = store_neighbors(@_);
# only enqueue if device is not already discovered, and
# discover_no_type config permits the discovery
foreach my $neighbor (@to_discover) {
my ($ip, $remote_type) = @$neighbor;
my $device = get_device($ip);
next if $device->in_storage;
my $remote_type_match = setting('discover_no_type');
if ($remote_type and $remote_type_match
and $remote_type =~ m/$remote_type_match/) {
debug sprintf ' queue - %s, type [%s] excluded by discover_no_type',
$ip, $remote_type;
next;
}
try {
# could fail if queued job already exists
schema('netdisco')->resultset('Admin')->create({
device => $ip,
action => 'discover',
status => 'queued',
});
};
try {
schema('netdisco')->resultset('Admin')->create({
device => $ip,
action => 'discover',
status => 'queued',
});
};
}
}
1;

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

View File

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

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

View File

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

View File

@@ -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.

View File

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

View File

@@ -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");

View File

@@ -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");

View File

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

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

View File

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

View File

@@ -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});

View File

@@ -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' => [

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

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

View File

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

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE device_port ADD COLUMN "manual_topo" bool DEFAULT false NOT NULL;
COMMIT;

View File

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

View File

@@ -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/],
};

View File

@@ -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,

View File

@@ -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/)
};

View File

@@ -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,

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

View File

@@ -5,7 +5,7 @@ use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::SNMP 'snmp_connect';
use App::Netdisco::Util::Device 'get_device';
use App::Netdisco::Util::DiscoverAndStore ':all';
use App::Netdisco::Core::Discover ':all';
use App::Netdisco::Daemon::Util ':all';
use NetAddr::IP::Lite ':lower';
@@ -14,29 +14,48 @@ use Role::Tiny;
use namespace::clean;
# queue a discover job for all devices known to Netdisco
sub refresh {
sub discoverall {
my ($self, $job) = @_;
my $devices = schema('netdisco')->resultset('Device')->get_column('ip');
my $jobqueue = schema('netdisco')->resultset('Admin');
schema('netdisco')->resultset('Admin')->populate([
map {{
device => $_,
schema('netdisco')->txn_do(sub {
# clean up user submitted jobs older than 1min,
# assuming skew between schedulers' clocks is not greater than 1min
$jobqueue->search({
action => 'discover',
status => 'queued',
}} ($devices->all)
]);
entered => { '<' => \"(now() - interval '1 minute')" },
})->delete;
# is scuppered by any user job submitted in last 1min (bad), or
# any similar job from another scheduler (good)
$jobqueue->populate([
map {{
device => $_,
action => 'discover',
status => 'queued',
}} ($devices->all)
]);
});
return job_done("Queued discover job for all devices");
}
# queue a discover job for one device, and its *new* neighbors
sub discover {
my ($self, $job) = @_;
my $host = NetAddr::IP::Lite->new($job->device);
my $device = get_device($host->addr);
my $snmp = snmp_connect($device);
if ($device->in_storage
and $device->vendor and $device->vendor eq 'netdisco') {
return job_done("Skipped discover for pseudo-device $host");
}
my $snmp = snmp_connect($device);
if (!defined $snmp) {
return job_error("discover failed: could not SNMP connect to $host");
}
@@ -47,42 +66,9 @@ sub discover {
store_vlans($device, $snmp);
store_power($device, $snmp);
store_modules($device, $snmp);
discover_new_neighbors($device, $snmp);
return job_done("Ended discover for $host");
}
# run find_neighbors on all known devices, and run discover on any
# newly found devices.
sub discovernew {
my ($self, $job) = @_;
my $devices = schema('netdisco')->resultset('Device')->get_column('ip');
schema('netdisco')->resultset('Admin')->populate([
map {{
device => $_,
action => 'discover_neighbors',
status => 'queued',
}} ($devices->all)
]);
return job_done("Queued discover_neighbors job for all devices");
}
sub discover_neighbors {
my ($self, $job) = @_;
my $host = NetAddr::IP::Lite->new($job->device);
my $device = get_device($host->addr);
my $snmp = snmp_connect($device);
if (!defined $snmp) {
return job_error("discover_neighbors failed: could not SNMP connect to $host");
}
find_neighbors($device, $snmp);
return job_done("Ended find_neighbors for $host");
return job_done("Ended discover for ". $host->addr);
}
1;

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

View File

@@ -11,12 +11,11 @@ use namespace::clean;
my $jobactions = {
map {$_ => undef} qw/
refresh
discovernew
discoverall
arpwalk
macwalk
/
# saveconfigs
# macwalk
# arpwalk
# nbtwalk
# backup
};

View 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

View File

@@ -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/

View File

@@ -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,

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

View File

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

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

View File

@@ -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';
};

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

View File

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

View File

@@ -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';

View File

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

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

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

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

View File

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

View File

@@ -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', {

View File

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

View File

@@ -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 };
};

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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+$/) {

View File

@@ -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+$/) {

View File

@@ -4,42 +4,43 @@ use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Try::Tiny;
ajax '/ajax/portcontrol' => sub {
try {
my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]',
param('device'), (param('port') || ''), param('field'),
(param('action') || ''), (param('value') || '');
send_error('Forbidden', 403)
unless var('user')->port_control;
send_error('No device/port/field', 400)
unless param('device') and param('port') and param('field');
my %action_map = (
'location' => 'location',
'contact' => 'contact',
'c_port' => 'portcontrol',
'c_name' => 'portname',
'c_vlan' => 'vlan',
'c_power' => 'power',
);
my $log = sprintf 'd:[%s] p:[%s] f:[%s]. a:[%s] v[%s]',
param('device'), (param('port') || ''), param('field'),
(param('action') || ''), (param('value') || '');
my $action = $action_map{ param('field') };
my $subaction = ($action =~ m/^(?:power|portcontrol)/
? (param('action') ."-other")
: param('value'));
my %action_map = (
'location' => 'location',
'contact' => 'contact',
'c_port' => 'portcontrol',
'c_name' => 'portname',
'c_vlan' => 'vlan',
'c_power' => 'power',
);
schema('netdisco')->resultset('Admin')->create({
device => param('device'),
port => param('port'),
action => $action,
subaction => $subaction,
status => 'queued',
username => session('user'),
userip => request->remote_address,
log => $log,
});
}
catch {
send_error('Failed to parse params or add DB record');
};
send_error('No action/value', 400)
unless (param('action') or param('value'));
my $action = $action_map{ param('field') };
my $subaction = ($action =~ m/^(?:power|portcontrol)/
? (param('action') ."-other")
: param('value'));
schema('netdisco')->resultset('Admin')->create({
device => param('device'),
port => param('port'),
action => $action,
subaction => $subaction,
status => 'queued',
username => session('user'),
userip => request->remote_address,
log => $log,
});
content_type('application/json');
to_json({});
@@ -47,7 +48,7 @@ ajax '/ajax/portcontrol' => sub {
ajax '/ajax/userlog' => sub {
my $user = session('user');
send_error('No username') unless $user;
send_error('No username', 400) unless $user;
my $rs = schema('netdisco')->resultset('Admin')->search({
username => $user,

View File

@@ -10,7 +10,7 @@ get '/report/*' => sub {
var(nav => 'reports');
template 'report', {
report => setting('reports')->{ $tag },
report => setting('_reports')->{ $tag },
};
};

View File

@@ -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

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

View File

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

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

View File

@@ -38,6 +38,9 @@ engines:
web_plugins:
- Inventory
- Report::DuplexMismatch
- AdminTask::PseudoDevice
- AdminTask::Topology
- AdminTask::JobQueue
- Search::Device
- Search::Node
- Search::VLAN

File diff suppressed because one or more lines are too long

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

View File

@@ -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 */

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 335 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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')

View File

@@ -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');
}

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

View 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 %]&nbsp;
[% END %]
</td>
[% IF row.status.search('^queued-') %]
<td class="nd_center-cell">Running on &quot;[% row.status.remove('^queued-') | html_entity %]&quot;</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>

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

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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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> &nbsp;' 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,14 +243,25 @@
[% 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>
[% END %]
</tbody>

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,4 +1,4 @@
<table class="table-bordered table-condensed table-striped">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>MAC</th>

View File

@@ -1,4 +1,4 @@
<table class="table-bordered table-condensed table-striped">
<table class="table table-bordered table-hover">
<thead>
<tr>
<th>MAC</th>

View File

@@ -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>

View File

@@ -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 %]

Some files were not shown because too many files have changed in this diff Show More