Merge branch 'master' into og-autoload

This commit is contained in:
Oliver Gorwits
2014-07-23 20:09:18 +01:00
39 changed files with 1415 additions and 105 deletions

View File

@@ -1,3 +1,91 @@
2.028012 - 2014-07-22
[BUG FIXES]
* UTF8 decode the remote type string in neighbor protocol
2.028011 - 2014-07-22
[BUG FIXES]
* Load Graph modules only if available
2.028010 - 2014-07-22
[NEW FEATURES]
* Node Monitor admin panel and netdisco-do action (beta)
[BUG FIXES]
* Hash deref in Nbtstat.pm
2.028008 - 2014-07-22
[BUG FIXES]
* Encode NetBIOS name, domain, user as UTF-8
2.028007 - 2014-07-21
[ENHANCEMENTS]
* Add 'graph' option to netdisco-do
[BUG FIXES]
* DNS Mismatch report incorrectly trimmed domain_suffix
* Fake one device aliases entry for devices not providing ip_index
* DataTables render function should check for type
2.028006 - 2014-07-21
[ENHANCEMENTS]
* Asynchronous NBTstat
[BUG FIXES]
* Device DNS Mismatch report: use name in the name column, and fix link
2.028005 - 2014-07-17
[BUG FIXES]
* Custom path handling for DataTables ajax calls
* [#116] Log true client IP when running through a proxy
* Use /etc/hosts for all forward/reverse DNS
2.028004 - 2014-07-16
[BUG FIXES]
* Close socket upon completion of async DNS resolution
2.028003 - 2014-07-15
[NEW FEATURES]
* GraphViz graph export (beta)
[BUG FIXES]
* No default for interactives made daemon die (H. Weber)
2.028001 - 2014-07-13
[NEW FEATURES]
* portctl_nameonly to limit port control to name only (F. Schiavarelli)
* add by_hostname config option to netdisco-rancid-export
[BUG FIXES]
* [ND1#117] unknown devices missing from inventory
* [#118] Fix table rendering on port search tab when VLAN is null
* Fix truncated port description change (F. Schiavarelli)
* Fix device port ordering in DataTables when Port Control is enabled
2.028000 - 2014-07-01 2.028000 - 2014-07-01
[NEW FEATURES] [NEW FEATURES]

View File

@@ -20,6 +20,7 @@ inc/Module/Install/Share.pm
inc/Module/Install/Win32.pm inc/Module/Install/Win32.pm
inc/Module/Install/WriteAll.pm inc/Module/Install/WriteAll.pm
lib/App/Netdisco.pm lib/App/Netdisco.pm
lib/App/Netdisco/AnyEvent/Nbtstat.pm
lib/App/Netdisco/Configuration.pm lib/App/Netdisco/Configuration.pm
lib/App/Netdisco/Core/Arpnip.pm lib/App/Netdisco/Core/Arpnip.pm
lib/App/Netdisco/Core/Discover.pm lib/App/Netdisco/Core/Discover.pm
@@ -80,6 +81,7 @@ lib/App/Netdisco/DB/Result/Virtual/DeviceLinks.pm
lib/App/Netdisco/DB/Result/Virtual/DevicePoeStatus.pm lib/App/Netdisco/DB/Result/Virtual/DevicePoeStatus.pm
lib/App/Netdisco/DB/Result/Virtual/DuplexMismatch.pm lib/App/Netdisco/DB/Result/Virtual/DuplexMismatch.pm
lib/App/Netdisco/DB/Result/Virtual/GenericReport.pm lib/App/Netdisco/DB/Result/Virtual/GenericReport.pm
lib/App/Netdisco/DB/Result/Virtual/NodeMonitor.pm
lib/App/Netdisco/DB/Result/Virtual/NodeWithAge.pm lib/App/Netdisco/DB/Result/Virtual/NodeWithAge.pm
lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm lib/App/Netdisco/DB/Result/Virtual/OrphanedDevices.pm
lib/App/Netdisco/DB/Result/Virtual/PhonesDiscovered.pm lib/App/Netdisco/DB/Result/Virtual/PhonesDiscovered.pm
@@ -151,7 +153,9 @@ lib/App/Netdisco/Manual/WritingPlugins.pod
lib/App/Netdisco/Util/Device.pm lib/App/Netdisco/Util/Device.pm
lib/App/Netdisco/Util/DNS.pm lib/App/Netdisco/Util/DNS.pm
lib/App/Netdisco/Util/ExpandParams.pm lib/App/Netdisco/Util/ExpandParams.pm
lib/App/Netdisco/Util/Graph.pm
lib/App/Netdisco/Util/Node.pm lib/App/Netdisco/Util/Node.pm
lib/App/Netdisco/Util/NodeMonitor.pm
lib/App/Netdisco/Util/Noop.pm lib/App/Netdisco/Util/Noop.pm
lib/App/Netdisco/Util/Permission.pm lib/App/Netdisco/Util/Permission.pm
lib/App/Netdisco/Util/Port.pm lib/App/Netdisco/Util/Port.pm
@@ -167,6 +171,7 @@ lib/App/Netdisco/Web/GenericReport.pm
lib/App/Netdisco/Web/Password.pm lib/App/Netdisco/Web/Password.pm
lib/App/Netdisco/Web/Plugin.pm lib/App/Netdisco/Web/Plugin.pm
lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm lib/App/Netdisco/Web/Plugin/AdminTask/JobQueue.pm
lib/App/Netdisco/Web/Plugin/AdminTask/NodeMonitor.pm
lib/App/Netdisco/Web/Plugin/AdminTask/OrphanedDevices.pm lib/App/Netdisco/Web/Plugin/AdminTask/OrphanedDevices.pm
lib/App/Netdisco/Web/Plugin/AdminTask/PollerPerformance.pm lib/App/Netdisco/Web/Plugin/AdminTask/PollerPerformance.pm
lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm lib/App/Netdisco/Web/Plugin/AdminTask/PseudoDevice.pm
@@ -292,6 +297,7 @@ share/public/javascripts/toastr.js
share/public/javascripts/underscore.min.js share/public/javascripts/underscore.min.js
share/views/admintask.tt share/views/admintask.tt
share/views/ajax/admintask/jobqueue.tt share/views/ajax/admintask/jobqueue.tt
share/views/ajax/admintask/nodemonitor.tt
share/views/ajax/admintask/orphaned.tt share/views/ajax/admintask/orphaned.tt
share/views/ajax/admintask/orphaned_csv.tt share/views/ajax/admintask/orphaned_csv.tt
share/views/ajax/admintask/performance.tt share/views/ajax/admintask/performance.tt

View File

@@ -51,12 +51,12 @@ requires:
Net::Domain: 1.23 Net::Domain: 1.23
Net::LDAP: 0 Net::LDAP: 0
Net::MAC: 2.103622 Net::MAC: 2.103622
Net::NBName: 0.26
NetAddr::IP: 4.068 NetAddr::IP: 4.068
Opcode: 1.07 Opcode: 1.07
Path::Class: 0.32 Path::Class: 0.32
Plack: 1.0023 Plack: 1.0023
Plack::Middleware::Expires: 0.03 Plack::Middleware::Expires: 0.03
Plack::Middleware::ReverseProxy: 0.15
Role::Tiny: 1.002005 Role::Tiny: 1.002005
SNMP::Info: 3.18 SNMP::Info: 3.18
SQL::Translator: 0.11016 SQL::Translator: 0.11016
@@ -81,4 +81,4 @@ resources:
homepage: http://netdisco.org/ homepage: http://netdisco.org/
license: http://opensource.org/licenses/bsd-license.php license: http://opensource.org/licenses/bsd-license.php
repository: git://git.code.sf.net/p/netdisco/netdisco-ng repository: git://git.code.sf.net/p/netdisco/netdisco-ng
version: 2.028000 version: 2.028012

View File

@@ -36,12 +36,12 @@ requires 'Net::Domain' => 1.23;
requires 'Net::DNS' => 0.72; requires 'Net::DNS' => 0.72;
requires 'Net::LDAP' => 0; requires 'Net::LDAP' => 0;
requires 'Net::MAC' => 2.103622; requires 'Net::MAC' => 2.103622;
requires 'Net::NBName' => 0.26;
requires 'NetAddr::IP' => 4.068; requires 'NetAddr::IP' => 4.068;
requires 'Opcode' => 1.07; requires 'Opcode' => 1.07;
requires 'Path::Class' => 0.32; requires 'Path::Class' => 0.32;
requires 'Plack' => 1.0023; requires 'Plack' => 1.0023;
requires 'Plack::Middleware::Expires' => 0.03; requires 'Plack::Middleware::Expires' => 0.03;
requires 'Plack::Middleware::ReverseProxy' => 0.15;
requires 'Role::Tiny' => 1.002005; requires 'Role::Tiny' => 1.002005;
requires 'Socket6' => 0.23; requires 'Socket6' => 0.23;
requires 'Starman' => 0.4008; requires 'Starman' => 0.4008;

View File

@@ -81,6 +81,19 @@ if (!length $action) {
with 'App::Netdisco::Daemon::Worker::Poller::Expiry'; with 'App::Netdisco::Daemon::Worker::Poller::Expiry';
with 'App::Netdisco::Daemon::Worker::Interactive::DeviceActions'; with 'App::Netdisco::Daemon::Worker::Interactive::DeviceActions';
with 'App::Netdisco::Daemon::Worker::Interactive::PortActions'; with 'App::Netdisco::Daemon::Worker::Interactive::PortActions';
use Module::Load ();
eval { Module::Load::load 'App::Netdisco::Util::Graph' };
sub graph {
App::Netdisco::Util::Graph::graph();
return ('done', 'Generated graph data.');
}
use App::Netdisco::Util::NodeMonitor ();
sub monitor {
App::Netdisco::Util::NodeMonitor::monitor();
return ('done', 'Generated monitor data.');
}
} }
my $worker = MyWorker->new(); my $worker = MyWorker->new();
@@ -152,6 +165,10 @@ Run an arpnip on the device (specified with C<-d>).
Run an nbtstat on the node (specified with C<-d>). Run an nbtstat on the node (specified with C<-d>).
=head2 graph
Generate GrapgViz graphs for the largest cluster of devices.
=head2 set_location =head2 set_location
Set the SNMP location field on the device (specified with C<-d>). Pass the Set the SNMP location field on the device (specified with C<-d>). Pass the

View File

@@ -43,15 +43,22 @@ use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Permission ':all'; use App::Netdisco::Util::Permission ':all';
my $settings = setting( 'rancid' ); my $settings = setting( 'rancid' );
my $domain_suffix = setting( 'domain_suffix' ) || '';
my $delimiter = $settings->{ 'delimiter' } || ':'; my $delimiter = $settings->{ 'delimiter' } || ':';
my $down_age = $settings->{ 'down_age' } || '1 day'; my $down_age = $settings->{ 'down_age' } || '1 day';
my $rancidhome = $settings->{ 'rancid_home' } || '/var/lib/rancid'; my $rancidhome = $settings->{ 'rancid_home' } || '/var/lib/rancid';
my $config_vendormap = $settings->{ 'vendormap' } || {}; my $config_vendormap = $settings->{ 'vendormap' } || {};
my $by_ip = {}; my $by_ip = {};
foreach my $g (@{$settings->{ 'by_ip' }}) { foreach my $g (@{$settings->{ 'by_ip' }}) {
$by_ip->{$g} = 1; $by_ip->{$g} = 1;
} }
my $by_hostname = {};
foreach my $g (@{$settings->{ 'by_hostname' }}) {
$by_hostname->{$g} = 1;
}
my @devices = schema('netdisco')->resultset('Device')->search({}, my @devices = schema('netdisco')->resultset('Device')->search({},
{ {
'+columns' => { '+columns' => {
@@ -92,13 +99,16 @@ foreach my $group (keys %$list) {
$vendor = $VENDORMAP{$vendormodel} || $VENDORMAP{$vendor}; $vendor = $VENDORMAP{$vendormodel} || $VENDORMAP{$vendor};
} }
if ( $config_vendormap->{$vendor} or $config_vendormap->{$vendormodel} ) { if ( $config_vendormap->{$vendor} or $config_vendormap->{$vendormodel} ) {
$vendor = $config_vendormap{$vendormodel} || $config_vendormap{$vendor}; $vendor = $config_vendormap->{$vendormodel} || $config_vendormap->{$vendor};
} }
if ($by_ip->{$group}) { if ($by_ip->{$group}) {
$name = $dev->ip; $name = $dev->ip;
} else { } else {
$name = ($dev->dns || $dev->name); $name = ($dev->dns || $dev->name);
} }
if ($by_hostname->{$group}) {
$name =~ s/$domain_suffix$//;
}
printf ROUTER "%s$delimiter%s$delimiter%s\n", $name, $vendor, printf ROUTER "%s$delimiter%s$delimiter%s\n", $name, $vendor,
$dev->get_column( 'old' ) ? "down" : "up"; $dev->get_column( 'old' ) ? "down" : "up";
} }
@@ -119,6 +129,7 @@ This script requires some configuration to be added to your Netdisco
down_age: '1 day' down_age: '1 day'
delimiter: ':' delimiter: ':'
by_ip: [ other ] by_ip: [ other ]
by_hostname: [ other2 ]
groups: groups:
switch: [ 'name:.*[Ss][Ww].*' ] switch: [ 'name:.*[Ss][Ww].*' ]
rtr: [ 'name:[rR]tr.*' ] rtr: [ 'name:[rR]tr.*' ]
@@ -172,6 +183,12 @@ Netdisco's "C<*_only>" settings, and accepts IP, prefix, device property.
List of RANCID Groups which will have Device IPs written to the RANCID List of RANCID Groups which will have Device IPs written to the RANCID
configuration file, instead of DNS or SNMP host names. configuration file, instead of DNS or SNMP host names.
=head2 C<by_hostname>
List of RANCID Groups which will have Device Hostname written to the RANCID
configuration file, instead of FQDN. This is done simply by stripping the
C<domain_suffix> configuration item from the FQDN.
=head1 SEE ALSO =head1 SEE ALSO
=over 4 =over 4

View File

@@ -31,6 +31,7 @@ my $home = ($ENV{NETDISCO_HOME} || $ENV{HOME});
set(session_dir => dir($home, 'netdisco-web-sessions')->stringify); set(session_dir => dir($home, 'netdisco-web-sessions')->stringify);
set plack_middlewares => [ set plack_middlewares => [
['Plack::Middleware::ReverseProxy'],
[ Expires => ( [ Expires => (
content_type => [qr{^application/javascript}, qr{^text/css}, qr{image}, qr{font}], content_type => [qr{^application/javascript}, qr{^text/css}, qr{image}, qr{font}],
expires => 'access plus 1 day', expires => 'access plus 1 day',

View File

@@ -4,7 +4,7 @@ use strict;
use warnings; use warnings;
use 5.010_000; use 5.010_000;
our $VERSION = '2.028000'; our $VERSION = '2.028012';
use App::Netdisco::Configuration; use App::Netdisco::Configuration;
use Module::Find (); use Module::Find ();

View File

@@ -0,0 +1,277 @@
package App::Netdisco::AnyEvent::Nbtstat;
use Socket qw(AF_INET SOCK_DGRAM inet_aton sockaddr_in);
use List::Util ();
use Carp ();
use AnyEvent (); BEGIN { AnyEvent::common_sense }
use AnyEvent::Util ();
sub new {
my ( $class, %args ) = @_;
my $interval = $args{interval};
# This default should generate ~ 50 requests per second
$interval = 0.2 unless defined $interval;
my $timeout = $args{timeout};
# Timeout should be 250ms according to RFC1002, but we're going to double
$timeout = 0.5 unless defined $timeout;
my $self = bless { interval => $interval, timeout => $timeout, %args },
$class;
Scalar::Util::weaken( my $wself = $self );
socket my $fh4, AF_INET, Socket::SOCK_DGRAM(), 0
or Carp::croak "Unable to create socket : $!";
AnyEvent::Util::fh_nonblocking $fh4, 1;
$self->{fh4} = $fh4;
$self->{rw4} = AE::io $fh4, 0, sub {
if ( my $peer = recv $fh4, my $resp, 2048, 0 ) {
$wself->_on_read( $resp, $peer );
}
};
# Nbtstat tasks
$self->{_tasks} = {};
return $self;
}
sub interval { @_ > 1 ? $_[0]->{interval} = $_[1] : $_[0]->{interval} }
sub timeout { @_ > 1 ? $_[0]->{timeout} = $_[1] : $_[0]->{timeout} }
sub nbtstat {
my ( $self, $host, $cb ) = @_;
my $ip = inet_aton($host);
my $port = 137;
my $request = {
host => $host,
results => {},
cb => $cb,
destination => scalar sockaddr_in( $port, $ip ),
};
$self->{_tasks}{ $request->{destination} } = $request;
my $delay = $self->interval * scalar keys %{ $self->{_tasks} || {} };
# There's probably a better way to throttle the sends
# but this will work for now since we currently don't support retries
my $w; $w = AE::timer $delay, 0, sub {
undef $w;
$self->_send_request($request);
};
return $self;
}
sub _on_read {
my ( $self, $resp, $peer ) = @_;
($resp) = $resp =~ /^(.*)$/s
if AnyEvent::TAINT && $self->{untaint};
# Find our task
my $request = $self->{_tasks}{$peer};
return unless $request;
$self->_store_result( $request, 'OK', $resp );
return;
}
sub _store_result {
my ( $self, $request, $status, $resp ) = @_;
my $results = $request->{results};
my @rr = ();
my $mac_address = "";
if ( $status eq 'OK' && length($resp) > 56 ) {
my $num_names = unpack( "C", substr( $resp, 56 ) );
my $name_data = substr( $resp, 57 );
for ( my $i = 0; $i < $num_names; $i++ ) {
my $rr_data = substr( $name_data, 18 * $i, 18 );
push @rr, _decode_rr($rr_data);
}
$mac_address = join "-",
map { sprintf "%02X", $_ }
unpack( "C*", substr( $name_data, 18 * $num_names, 6 ) );
$results = {
'status' => 'OK',
'names' => \@rr,
'mac_address' => $mac_address
};
}
elsif ( $status eq 'OK' ) {
$results = { 'status' => 'SHORT' };
}
else {
$results = { 'status' => $status };
}
# Clear request specific data
delete $request->{timer};
# Cleanup
delete $self->{_tasks}{ $request->{destination} };
# Done
$request->{cb}->($results);
undef $request;
return;
}
sub _send_request {
my ( $self, $request ) = @_;
my $msg = "";
# We use process id as identifier field, since don't have a need to
# unique responses beyond host / port queried
$msg .= pack( "n*", $$, 0, 1, 0, 0, 0 );
$msg .= _encode_name( "*", "\x00", 0 );
$msg .= pack( "n*", 0x21, 0x0001 );
$request->{start} = time;
$request->{timer} = AE::timer $self->timeout, 0, sub {
$self->_store_result( $request, 'TIMEOUT' );
};
my $fh = $self->{fh4};
send $fh, $msg, 0, $request->{destination}
or $self->_store_result( $request, 'ERROR' );
return;
}
sub _encode_name {
my $name = uc(shift);
my $pad = shift || "\x20";
my $suffix = shift || 0x00;
$name .= $pad x ( 16 - length($name) );
substr( $name, 15, 1, chr( $suffix & 0xFF ) );
my $encoded_name = "";
for my $c ( unpack( "C16", $name ) ) {
$encoded_name .= chr( ord('A') + ( ( $c & 0xF0 ) >> 4 ) );
$encoded_name .= chr( ord('A') + ( $c & 0xF ) );
}
# Note that the _encode_name function doesn't add any scope,
# nor does it calculate the length (32), it just prefixes it
return "\x20" . $encoded_name . "\x00";
}
sub _decode_rr {
my $rr_data = shift;
my @nodetypes = qw/B-node P-node M-node H-node/;
my ( $name, $suffix, $flags ) = unpack( "a15Cn", $rr_data );
$name =~ tr/\x00-\x19/\./; # replace ctrl chars with "."
$name =~ s/\s+//g;
my $rr = {};
$rr->{'name'} = $name;
$rr->{'suffix'} = $suffix;
$rr->{'G'} = ( $flags & 2**15 ) ? "GROUP" : "UNIQUE";
$rr->{'ONT'} = $nodetypes[ ( $flags >> 13 ) & 3 ];
$rr->{'DRG'} = ( $flags & 2**12 ) ? "Deregistering" : "Registered";
$rr->{'CNF'} = ( $flags & 2**11 ) ? "Conflict" : "";
$rr->{'ACT'} = ( $flags & 2**10 ) ? "Active" : "Inactive";
$rr->{'PRM'} = ( $flags & 2**9 ) ? "Permanent" : "";
return $rr;
}
1;
__END__
=head1 NAME
App::Netdisco::AnyEvent::Nbtstat - Request NetBIOS node status with AnyEvent
=head1 SYNOPSIS
use App::Netdisco::AnyEvent::Nbtstat;;
my $request = App::Netdisco::AnyEvent::Nbtstat->new();
my $cv = AE::cv;
$request->nbtstat(
'127.0.0.1',
sub {
my $result = shift;
print "MAC: ", $result->{'mac_address'} || '', " ";
print "Status: ", $result->{'status'}, "\n";
printf '%3s %-18s %4s %-18s', '', 'Name', '', 'Type'
if ( $result->{'status'} eq 'OK' );
print "\n";
for my $rr ( @{ $result->{'names'} } ) {
printf '%3s %-18s <%02s> %-18s', '', $rr->{'name'},
$rr->{'suffix'},
$rr->{'G'};
print "\n";
}
$cv->send;
}
);
$cv->recv;
=head1 DESCRIPTION
L<App::Netdisco::AnyEvent::Nbtstat> is an asynchronous AnyEvent NetBIOS node
status requester.
=head1 ATTRIBUTES
L<App::Netdisco::AnyEvent::Nbtstat> implements the following attributes.
=head2 C<interval>
my $interval = $request->interval;
$request->interval(1);
Interval between requests, defaults to 0.02 seconds.
=head2 C<timeout>
my $timeout = $request->timeout;
$request->timeout(2);
Maximum request response time, defaults to 0.5 seconds.
=head1 METHODS
L<App::Netdisco::AnyEvent::Nbtstat> implements the following methods.
=head2 C<nbtstat>
$request->nbtstat($ip, sub {
my $result = shift;
});
Perform a NetBIOS node status request of $ip.
=head1 SEE ALSO
L<AnyEvent>
=cut

View File

@@ -41,8 +41,9 @@ setting('plugins')->{DBIC}->{daemon} = {
schema_class => 'App::Netdisco::Daemon::DB', schema_class => 'App::Netdisco::Daemon::DB',
}; };
# default queue model is Pg # defaults for workers
setting('workers')->{queue} ||= 'PostgreSQL'; setting('workers')->{queue} ||= 'PostgreSQL';
setting('workers')->{interactives} ||= 1;
# force skipped DNS resolution, if unset # force skipped DNS resolution, if unset
setting('dns')->{hosts_file} ||= '/etc/hosts'; setting('dns')->{hosts_file} ||= '/etc/hosts';

View File

@@ -146,6 +146,10 @@ sub store_device {
scalar @aliases, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}; scalar @aliases, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'};
my $resolved_aliases = hostnames_resolve_async(\@aliases); my $resolved_aliases = hostnames_resolve_async(\@aliases);
# fake one aliases entry for devices not providing ip_index
push @$resolved_aliases, { alias => $device->ip, dns => $hostname }
if 0 == scalar @aliases;
# VTP Management Domain -- assume only one. # VTP Management Domain -- assume only one.
my $vtpdomains = $snmp->vtp_d_name; my $vtpdomains = $snmp->vtp_d_name;
my $vtpdomain; my $vtpdomain;
@@ -682,7 +686,7 @@ sub store_neighbors {
my $remote_ip = $c_ip->{$entry}; my $remote_ip = $c_ip->{$entry};
my $remote_ipad = NetAddr::IP::Lite->new($remote_ip); my $remote_ipad = NetAddr::IP::Lite->new($remote_ip);
my $remote_port = undef; my $remote_port = undef;
my $remote_type = $c_platform->{$entry} || ''; my $remote_type = Encode::decode('UTF-8', $c_platform->{$entry} || '');
my $remote_id = Encode::decode('UTF-8', $c_id->{$entry}); my $remote_id = Encode::decode('UTF-8', $c_id->{$entry});
my $remote_cap = $c_cap->{$entry} || []; my $remote_cap = $c_cap->{$entry} || [];

View File

@@ -5,11 +5,12 @@ use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Node 'check_mac'; use App::Netdisco::Util::Node 'check_mac';
use NetAddr::IP::Lite ':lower'; use NetAddr::IP::Lite ':lower';
use Net::NBName; use App::Netdisco::AnyEvent::Nbtstat;
use Encode;
use base 'Exporter'; use base 'Exporter';
our @EXPORT = (); our @EXPORT = ();
our @EXPORT_OK = qw/ do_nbtstat store_nbt /; our @EXPORT_OK = qw/ nbtstat_resolve_async store_nbt /;
our %EXPORT_TAGS = (all => \@EXPORT_OK); our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME =head1 NAME
@@ -25,42 +26,64 @@ subroutines.
=head1 EXPORT_OK =head1 EXPORT_OK
=head2 do_nbtstat( $node ) =head2 nbtstat_resolve_async( $ips )
Connects to node and gets NetBIOS information. Then adds entries to This method uses an asynchronous AnyEvent NetBIOS node status requester
node_nbt table. C<App::Netdisco::AnyEvent::Nbtstat>.
Returns whether a node is answering netbios calls or not. Given a reference to an array of hashes will connects to the C<IPv4> of a
node and gets NetBIOS node status information.
Returns the supplied reference to an array of hashes with MAC address,
NetBIOS name, NetBIOS domain/workgroup, NetBIOS user, and NetBIOS server
service status for addresses which responded.
=cut =cut
sub do_nbtstat { sub nbtstat_resolve_async {
my ($host, $now) = @_; my $ips = shift;
my $ip = NetAddr::IP::Lite->new($host) or return;
unless ( $ip->version() == 4 ) { my $timeout = setting('nbtstat_timeout') || 1;
debug ' nbtstat only supports IPv4, invalid ip %s', $ip->addr; my $interval = setting('nbtstat_interval') || 0.02;
return;
my $stater = App::Netdisco::AnyEvent::Nbtstat->new(
timeout => $timeout,
interval => $interval
);
# Set up the condvar
my $cv = AE::cv;
$cv->begin( sub { shift->send } );
foreach my $hash_ref (@$ips) {
my $ip = $hash_ref->{'ip'};
$cv->begin;
$stater->nbtstat(
$ip,
sub {
my $res = shift;
_filter_nbname( $ip, $hash_ref, $res );
$cv->end;
}
);
} }
my $nb = Net::NBName->new; # Decrement the cv counter to cancel out the send declaration
my $ns = $nb->node_status( $ip->addr ); $cv->end;
# Check for NetBIOS Info # Wait for the resolver to perform all resolutions
return unless $ns; $cv->recv;
my $nbname = _filter_nbname( $ip->addr, $ns ); # Close sockets
undef $stater;
if ($nbname) { return $ips;
store_nbt($nbname, $now);
}
return 1;
} }
# filter nbt names / information # filter nbt names / information
sub _filter_nbname { sub _filter_nbname {
my $ip = shift; my $ip = shift;
my $hash_ref = shift;
my $node_status = shift; my $node_status = shift;
my $server = 0; my $server = 0;
@@ -68,10 +91,10 @@ sub _filter_nbname {
my $domain = ''; my $domain = '';
my $nbuser = ''; my $nbuser = '';
for my $rr ( $node_status->names ) { for my $rr ( @{$node_status->{'names'}} ) {
my $suffix = defined $rr->suffix ? $rr->suffix : -1; my $suffix = defined $rr->{'suffix'} ? $rr->{'suffix'} : -1;
my $G = defined $rr->G ? $rr->G : ''; my $G = defined $rr->{'G'} ? $rr->{'G'} : '';
my $name = defined $rr->name ? $rr->name : ''; my $name = defined $rr->{'name'} ? $rr->{'name'} : '';
if ( $suffix == 0 and $G eq "GROUP" ) { if ( $suffix == 0 and $G eq "GROUP" ) {
$domain = $name; $domain = $name;
@@ -88,11 +111,11 @@ sub _filter_nbname {
} }
unless ($nbname) { unless ($nbname) {
debug ' nbtstat no computer name found for %s', $ip; debug sprintf ' nbtstat no computer name found for %s', $ip;
return; return;
} }
my $mac = $node_status->mac_address || ''; my $mac = $node_status->{'mac_address'} || '';
unless ( check_mac( $ip, $mac ) ) { unless ( check_mac( $ip, $mac ) ) {
@@ -101,23 +124,23 @@ sub _filter_nbname {
->single( { ip => $ip, -bool => 'active' } ); ->single( { ip => $ip, -bool => 'active' } );
if ( !defined $node_ip ) { if ( !defined $node_ip ) {
debug ' no MAC for %s returned by nbtstat or in DB', $ip; debug sprintf ' no MAC for %s returned by nbtstat or in DB', $ip;
return; return;
} }
$mac = $node_ip->mac; $mac = $node_ip->mac;
} }
return { $hash_ref->{'ip'} = $ip;
ip => $ip, $hash_ref->{'mac'} = $mac;
mac => $mac, $hash_ref->{'nbname'} = Encode::decode('UTF-8', $nbname);
nbname => $nbname, $hash_ref->{'domain'} = Encode::decode('UTF-8', $domain);
domain => $domain, $hash_ref->{'server'} = $server;
server => $server, $hash_ref->{'nbuser'} = Encode::decode('UTF-8', $nbuser);
nbuser => $nbuser
}; return;
} }
=head2 store_nbt($nb_hash_ref, $now?) =item store_nbt($nb_hash_ref, $now?)
Stores entries in C<node_nbt> table from the provided hash reference; MAC Stores entries in C<node_nbt> table from the provided hash reference; MAC
C<mac>, IP C<ip>, Unique NetBIOS Node Name C<nbname>, NetBIOS Domain or C<mac>, IP C<ip>, Unique NetBIOS Node Name C<nbname>, NetBIOS Domain or

View File

@@ -17,9 +17,8 @@ SELECT *
FROM device FROM device
WHERE dns IS NULL WHERE dns IS NULL
OR name IS NULL OR name IS NULL
OR lower(trim(TRAILING ? OR regexp_replace(lower(dns), ? || '$', '')
FROM dns)::text) != lower(trim(TRAILING ? != regexp_replace(lower(name), ? || '$', '')
FROM name)::text)
ENDSQL ENDSQL
1; 1;

View File

@@ -0,0 +1,49 @@
package App::Netdisco::DB::Result::Virtual::NodeMonitor;
use strict;
use warnings;
use utf8;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('node_monitor_virtual');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT nm.why, nm.cc, trim(trailing '.' from trim(trailing '0123456789' from date::text)) as date,
n.mac, n.switch, n.port,
d.name, d.location,
dp.name AS portname
FROM node_monitor nm, node n, device d, device_port dp
WHERE nm.mac = n.mac
AND nm.active
AND nm.cc IS NOT NULL
AND d.ip = n.switch
AND dp.ip = n.switch
AND dp.port = n.port
AND d.last_macsuck = n.time_last
ENDSQL
__PACKAGE__->add_columns(
"why",
{ data_type => "text", is_nullable => 1 },
"cc",
{ data_type => "text", is_nullable => 0 },
"date",
{ data_type => "timestamp", is_nullable => 0 },
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"switch",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"name",
{ data_type => "text", is_nullable => 0 },
"location",
{ data_type => "text", is_nullable => 1 },
"portname",
{ data_type => "text", is_nullable => 0 },
);
1;

View File

@@ -23,6 +23,10 @@ sub set_portcontrol {
return job_error("Cannot alter port: $reconfig_check") return job_error("Cannot alter port: $reconfig_check")
if $reconfig_check; if $reconfig_check;
# need to remove "-other" which appears for power/portcontrol
(my $sa = $job->subaction) =~ s/-\w+//;
$job->subaction($sa);
return _set_port_generic($job, 'up_admin'); return _set_port_generic($job, 'up_admin');
} }
@@ -50,7 +54,7 @@ sub _set_port_generic {
my $ip = $job->device; my $ip = $job->device;
my $pn = $job->port; my $pn = $job->port;
(my $data = $job->subaction) =~ s/-\w+//; my $data = $job->subaction;
my $port = get_port($ip, $pn) my $port = get_port($ip, $pn)
or return job_error("Unknown port name [$pn] on device [$ip]"); or return job_error("Unknown port name [$pn] on device [$ip]");

View File

@@ -3,7 +3,7 @@ package App::Netdisco::Daemon::Worker::Poller::Nbtstat;
use Dancer qw/:moose :syntax :script/; use Dancer qw/:moose :syntax :script/;
use Dancer::Plugin::DBIC 'schema'; use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Core::Nbtstat 'do_nbtstat'; use App::Netdisco::Core::Nbtstat qw/nbtstat_resolve_async store_nbt/;
use App::Netdisco::Util::Node 'is_nbtstatable'; use App::Netdisco::Util::Node 'is_nbtstatable';
use App::Netdisco::Util::Device qw/get_device is_discoverable/; use App::Netdisco::Util::Device qw/get_device is_discoverable/;
use App::Netdisco::Daemon::Util ':all'; use App::Netdisco::Daemon::Util ':all';
@@ -33,7 +33,7 @@ sub nbtstat {
} }
# get list of nodes on device # get list of nodes on device
my $interval = (setting('nbt_max_age') || 7) . ' day'; my $interval = (setting('nbtstat_max_age') || 7) . ' day';
my $rs = schema('netdisco')->resultset('NodeIp')->search({ my $rs = schema('netdisco')->resultset('NodeIp')->search({
-bool => 'me.active', -bool => 'me.active',
-bool => 'nodes.active', -bool => 'nodes.active',
@@ -46,10 +46,25 @@ sub nbtstat {
})->ip_version(4); })->ip_version(4);
my @nodes = $rs->get_column('ip')->all; my @nodes = $rs->get_column('ip')->all;
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
$self->_single_node_body('nbtstat', $_, $now) # Unless we have IP's don't bother
for @nodes; if (scalar @nodes) {
# filter exclusions from config
@nodes = grep { is_nbtstatable( $_ ) } @nodes;
# setup the hash nbtstat_resolve_async expects
my @ips = map {+{'ip' => $_}} @nodes;
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
my $resolved_nodes = nbtstat_resolve_async(\@ips);
# update node_nbt with status entries
foreach my $result (@$resolved_nodes) {
if (defined $result->{'nbname'}) {
store_nbt($result, $now);
}
}
}
return job_done("Ended nbtstat for ". $host->addr); return job_done("Ended nbtstat for ". $host->addr);
} }

View File

@@ -681,6 +681,20 @@ Value: Number. Default: 7.
The maximum age of a node in days for it to be checked for NetBIOS The maximum age of a node in days for it to be checked for NetBIOS
information. information.
=head3 C<nbtstat_interval>
Value: Number. Default: 0.02.
Interval between nbtstat requests in each poller. Defaults to 0.02 seconds,
equating to 50 requests per second per poller.
=head3 C<nbtstat_timeout>
Value: Number. Default: 1.
Seconds nbtstat will wait for a response before time out. Accepts fractional
seconds as well as integers.
=head3 C<expire_devices> =head3 C<expire_devices>
Value: Number of Days. Value: Number of Days.
@@ -782,6 +796,14 @@ field to use as the management IP address for a device.
Value: Boolean. Default: C<true>. Value: Boolean. Default: C<true>.
Set to false to prevent users from changing the default VLAN on an interface. Set to false to prevent users from changing the default VLAN on an interface.
This setting has no effect when C<portctl_nameonly> below is set to true.
=head3 C<portctl_nameonly>
Value: Boolean. Default: C<false>.
Set to true to limit port control action to only changing the interface name
(description).
=head3 C<portctl_nophones> =head3 C<portctl_nophones>

View File

@@ -8,6 +8,13 @@ use Net::DNS;
use AnyEvent::DNS; use AnyEvent::DNS;
use NetAddr::IP::Lite ':lower'; use NetAddr::IP::Lite ':lower';
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/
hostname_from_ip hostnames_resolve_async ipv4_from_hostname
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
# AE::DNS::EtcHosts only works for A/AAAA/SRV, but we want PTR. # AE::DNS::EtcHosts only works for A/AAAA/SRV, but we want PTR.
# this loads+parses /etc/hosts file using AE. dirty hack. # this loads+parses /etc/hosts file using AE. dirty hack.
use AnyEvent::Socket 'format_address'; use AnyEvent::Socket 'format_address';
@@ -15,12 +22,10 @@ use AnyEvent::DNS::EtcHosts;
AnyEvent::DNS::EtcHosts::_load_hosts_unless(sub{},AE::cv); AnyEvent::DNS::EtcHosts::_load_hosts_unless(sub{},AE::cv);
no AnyEvent::DNS::EtcHosts; # unimport no AnyEvent::DNS::EtcHosts; # unimport
use base 'Exporter'; our %HOSTS = ();
our @EXPORT = (); $HOSTS{$_} = [ map { [ $_ ? (format_address $_->[0]) : '' ] }
our @EXPORT_OK = qw/ @{$AnyEvent::DNS::EtcHosts::HOSTS{$_}} ]
hostname_from_ip hostnames_resolve_async ipv4_from_hostname for keys %AnyEvent::DNS::EtcHosts::HOSTS;
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME =head1 NAME
@@ -47,6 +52,13 @@ sub hostname_from_ip {
my $ip = shift; my $ip = shift;
return unless $ip; return unless $ip;
# check /etc/hosts file and short-circuit if found
foreach my $name (reverse sort keys %HOSTS) {
if ($HOSTS{$name}->[0]->[0] eq $ip) {
return $name;
}
}
my $res = Net::DNS::Resolver->new; my $res = Net::DNS::Resolver->new;
my $query = $res->search($ip); my $query = $res->search($ip);
@@ -72,6 +84,12 @@ sub ipv4_from_hostname {
my $name = shift; my $name = shift;
return unless $name; return unless $name;
# check /etc/hosts file and short-circuit if found
if (exists $HOSTS{$name} and $HOSTS{$name}->[0]->[0]) {
my $ip = NetAddr::IP::Lite->new($HOSTS{$name}->[0]->[0]);
return $ip->addr if $ip and $ip->bits == 32;
}
my $res = Net::DNS::Resolver->new; my $res = Net::DNS::Resolver->new;
my $query = $res->search($name); my $query = $res->search($name);
@@ -92,9 +110,7 @@ resolver C<AnyEvent::DNS>.
Given a reference to an array of hashes will resolve the C<IPv4> or C<IPv6> Given a reference to an array of hashes will resolve the C<IPv4> or C<IPv6>
address in the C<ip> or C<alias> key of each hash into its hostname which address in the C<ip> or C<alias> key of each hash into its hostname which
will be inserted in the C<dns> key of the hash. The resolver does also will be inserted in the C<dns> key of the hash.
forward-lookups to verify that the resolved hostnames point to the
address.
Returns the supplied reference to an array of hashes with dns values for Returns the supplied reference to an array of hashes with dns values for
addresses which resolved. addresses which resolved.
@@ -103,12 +119,6 @@ addresses which resolved.
sub hostnames_resolve_async { sub hostnames_resolve_async {
my $ips = shift; my $ips = shift;
my $resolver = AnyEvent::DNS->new();
my %HOSTS = ();
$HOSTS{$_} = [ map { [ $_ ? (format_address $_->[0]) : '' ] }
@{$AnyEvent::DNS::EtcHosts::HOSTS{$_}} ]
for keys %AnyEvent::DNS::EtcHosts::HOSTS;
# Set up the condvar # Set up the condvar
my $done = AE::cv; my $done = AE::cv;
@@ -137,6 +147,9 @@ sub hostnames_resolve_async {
# Wait for the resolver to perform all resolutions # Wait for the resolver to perform all resolutions
$done->recv; $done->recv;
# Remove reference to resolver so that we close sockets
undef $AnyEvent::DNS::RESOLVER if $AnyEvent::DNS::RESOLVER;
return $ips; return $ips;
} }

View File

@@ -0,0 +1,479 @@
package App::Netdisco::Util::Graph;
use App::Netdisco;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::DNS qw/hostname_from_ip ipv4_from_hostname/;
use Graph::Undirected ();
use GraphViz ();
use base 'Exporter';
our @EXPORT = ('graph');
our @EXPORT_OK = qw/
graph_each
graph_addnode
make_graph
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
# nothing to see here, please move along...
our ($ip, $label, $isdev, $devloc, %GRAPH, %GRAPH_SPEED);
=head1 NAME
App::Netdisco::Util::Graph
=head1 SYNOPSIS
$ brew install graphviz <-- install graphviz on your system
$ ~/bin/localenv bash
$ cpanm --notest Graph GraphViz
$ mkdir ~/graph
use App::Netdisco::Util::Graph;
graph;
=head1 DESCRIPTION
Generate GraphViz output from Netdisco data. Requires that the L<Graph> and
L<GraphViz> distributions be installed.
Requires the same config as for Netdisco 1, but within a C<graph> key. See
C<share/config.yml> in the source distribution for an example.
The C<graph> subroutine is exported by default. The C<:all> tag will export
all subroutines.
=head1 EXPORT
=over 4
=item graph()
Creates netmap of network.
=back
=cut
sub graph {
my %CONFIG = %{ setting('graph') };
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
my $month = sprintf("%d%02d",$year+1900,$mon+1);
info "graph() - Creating Graphs";
my $G = make_graph();
unless (defined $G){
print "graph() - make_graph() failed. Try running with debug (-D).";
return;
}
my @S = $G->connected_components;
# Count number of nodes in each subgraph
my %S_count;
for (my $i=0;$i< scalar @S;$i++){
$S_count{$i} = scalar @{$S[$i]};
}
foreach my $subgraph (sort { $S_count{$b} <=> $S_count{$a} } keys %S_count){
my $SUBG = $G->copy;
print "\$S[$subgraph] has $S_count{$subgraph} nodes.\n";
# Remove other subgraphs from this one
my %S_notme = %S_count;
delete $S_notme{$subgraph};
foreach my $other (keys %S_notme){
print "Removing Non-connected nodes: ",join(',',@{$S[$other]}),"\n";
$SUBG->delete_vertices(@{$S[$other]})
}
# Create the subgraph
my $timeout = defined $CONFIG{graph_timeout} ? $CONFIG{graph_timeout} : 60;
eval {
alarm($timeout*60);
graph_each($SUBG,'');
alarm(0);
};
if ($@) {
if ($@ =~ /timeout/){
print "! Creating Graph timed out!\n";
} else {
print "\n$@\n";
}
}
# Facility to create subgraph for each non-connected network segment.
# Right now, let's just make the biggest one only.
last;
}
}
=head1 EXPORT_OK
=over 4
=item graph_each($graph_obj, $name)
Generates subgraph. Does actual GraphViz calls.
=cut
sub graph_each {
my ($G, $name) = @_;
my %CONFIG = %{ setting('graph') };
info "Creating new Graph";
my $graph_defs = {
'bgcolor' => $CONFIG{graph_bg} || 'black',
'color' => $CONFIG{graph_color} || 'white',
'overlap' => $CONFIG{graph_overlap} || 'scale',
'fontpath'=> _homepath('graph_fontpath',''),
'ranksep' => $CONFIG{graph_ranksep} || 0.3,
'nodesep' => $CONFIG{graph_nodesep} || 2,
'ratio' => $CONFIG{graph_ratio} || 'compress',
'splines' => ($CONFIG{graph_splines} ? 'true' : 'false'),
'fontcolor' => $CONFIG{node_fontcolor} || 'white',
'fontname' => $CONFIG{node_font} || 'lucon',
'fontsize' => $CONFIG{node_fontsize} || 12,
};
my $edge_defs = {
'color' => $CONFIG{edge_color} || 'wheat',
};
my $node_defs = {
'shape' => $CONFIG{node_shape} || 'box',
'fillcolor' => $CONFIG{node_fillcolor} || 'dimgrey',
'fontcolor' => $CONFIG{node_fontcolor} || 'white',
'style' => $CONFIG{node_style} || 'filled',
'fontname' => $CONFIG{node_font} || 'lucon',
'fontsize' => $CONFIG{node_fontsize} || 12,
'fixedsize' => ($CONFIG{node_fixedsize} ? 'true' : 'false'),
};
$node_defs->{height} = $CONFIG{node_height} if defined $CONFIG{node_height};
$node_defs->{width} = $CONFIG{node_width} if defined $CONFIG{node_width};
my $epsilon = undef;
if (defined $CONFIG{graph_epsilon}){
$epsilon = "0." . '0' x $CONFIG{graph_epsilon} . '1';
}
my %gv = (
directed => 0,
layout => $CONFIG{graph_layout} || 'twopi',
graph => $graph_defs,
node => $node_defs,
edge => $edge_defs,
width => $CONFIG{graph_x} || 30,
height => $CONFIG{graph_y} || 30,
epsilon => $epsilon,
);
my $gv = GraphViz->new(%gv);
my %node_map = ();
my @nodes = $G->vertices;
foreach my $dev (@nodes){
my $node_name = graph_addnode($gv,$dev);
$node_map{$dev} = $node_name;
}
my $root_ip = defined $CONFIG{root_device}
? (ipv4_from_hostname($CONFIG{root_device}) || $CONFIG{root_device})
: undef;
if (defined $root_ip and defined $node_map{$root_ip}){
my $gv_root_name = $gv->_quote_name($root_ip);
if (defined $gv_root_name){
$gv->{GRAPH_ATTRS}->{root}=$gv_root_name;
}
}
my @edges = $G->edges;
while (my $e = shift @edges){
my $link = $e->[0];
my $dest = $e->[1];
my $speed = $GRAPH_SPEED{$link}->{$dest}->{speed};
if (!defined($speed)) {
info " ! No link speed for $link -> $dest";
$speed = 0;
}
my %edge = ();
my $val = ''; my $suffix = '';
if ($speed =~ /^([\d.]+)\s+([a-z])bps$/i) {
$val = $1; $suffix = $2;
}
if ( ($suffix eq 'k') or ($speed =~ m/(t1|ds3)/i) ){
$edge{color} = 'green';
$edge{style} = 'dotted';
}
if ($suffix eq 'M'){
if ($val < 10.0){
$edge{color} = 'green';
#$edge{style} = 'dotted';
$edge{style} = 'dashed';
} elsif ($val < 100.0){
$edge{color} = '#8b7e66';
#$edge{style} = 'normal';
$edge{style} = 'solid';
} else {
$edge{color} = '#ffe7ba';
$edge{style} = 'solid';
}
}
if ($suffix eq 'G'){
#$edge{style} = 'bold';
$edge{color} = 'cyan1';
}
# Add extra styles to edges (mainly for modifying width)
if(defined $CONFIG{edge_style}) {
$edge{style} .= "," . $CONFIG{edge_style};
}
$gv->add_edge($link => $dest, %edge );
}
info "Ignore all warnings about node size";
if (defined $CONFIG{graph_raw} and $CONFIG{graph_raw}){
my $graph_raw = _homepath('graph_raw');
info " Creating raw graph: $graph_raw";
$gv->as_canon($graph_raw);
}
if (defined $CONFIG{graph} and $CONFIG{graph}){
my $graph_gif = _homepath('graph');
info " Creating graph: $graph_gif";
$gv->as_gif($graph_gif);
}
if (defined $CONFIG{graph_png} and $CONFIG{graph_png}){
my $graph_png = _homepath('graph_png');
info " Creating png graph: $graph_png";
$gv->as_png($graph_png);
}
if (defined $CONFIG{graph_map} and $CONFIG{graph_map}){
my $graph_map = _homepath('graph_map');
info " Creating CMAP : $graph_map";
$gv->as_cmap($graph_map);
}
if (defined $CONFIG{graph_svg} and $CONFIG{graph_svg}){
my $graph_svg = _homepath('graph_svg');
info " Creating SVG : $graph_svg";
$gv->as_svg($graph_svg);
}
}
=item graph_addnode($graphviz_obj, $node_ip)
Checks for mapping settings in config file and adds node to the GraphViz
object.
=cut
sub graph_addnode {
my $gv = shift;
my %CONFIG = %{ setting('graph') };
my %node = ();
$ip = shift;
$label = $GRAPH{$ip}->{dns};
$isdev = $GRAPH{$ip}->{isdev};
$devloc = $GRAPH{$ip}->{location};
$label = "($ip)" unless defined $label;
my $domain_suffix = setting('domain_suffix') || '';
$label =~ s/$domain_suffix$//;
$node{label} = $label;
# Dereferencing the scalar by name below
# requires that the variable be non-lexical (not my)
# we'll create some local non-lexical versions
# that will expire at the end of this block
# Node Mappings
foreach my $map (@{ $CONFIG{'node_map'} || [] }){
my ($var, $regex, $attr, $val) = split(':', $map);
{ no strict 'refs';
$var = ${"$var"};
}
next unless defined $var;
if ($var =~ /$regex/) {
debug " graph_addnode - Giving node $ip $attr = $val";
$node{$attr} = $val;
}
}
# URL for image maps FIXME for non-root hosting
if ($isdev) {
$node{URL} = "/device?&q=$ip";
}
else {
$node{URL} = "/search?tab=node&q=$ip";
# Overrides any colors given to nodes above. Bug 1094208
$node{fillcolor} = $CONFIG{'node_problem'} || 'red';
}
if ($CONFIG{'graph_clusters'} && $devloc) {
# This odd construct works around a bug in GraphViz.pm's
# quoting of cluster names. If it has a name with spaces,
# it'll just quote it, resulting in creating a subgraph name
# of cluster_"location with spaces". This is an illegal name
# according to the dot grammar, so if the name matches the
# problematic regexp we make GraphViz.pm generate an internal
# name by using a leading space in the name.
#
# This is bug ID 16912 at rt.cpan.org -
# http://rt.cpan.org/NoAuth/Bug.html?id=16912
#
# Another bug, ID 11514, prevents us from using a combination
# of name and label attributes to hide the extra space from
# the user. However, since it's just a space, hopefully it
# won't be too noticable.
my($loc) = $devloc;
$loc = " " . $loc if ($loc =~ /^[a-zA-Z](\w| )*$/);
$node{cluster} = { name => $loc };
}
my $rv = $gv->add_node($ip, %node);
return $rv;
}
=item make_graph()
Returns C<Graph::Undirected> object that represents the discovered network.
Graph is made by loading all the C<device_port> entries that have a neighbor,
using them as edges. Then each device seen in those entries is added as a
vertex.
Nodes without topology information are not included.
=back
=cut
sub make_graph {
my $G = Graph::Undirected->new();
my $devices = schema('netdisco')->resultset('Device')
->search({}, { columns => [qw/ip dns location /] });
my $links = schema('netdisco')->resultset('DevicePort')
->search({remote_ip => { -not => undef }},
{ columns => [qw/ip remote_ip speed remote_type/]});
my %aliases = map {$_->alias => $_->ip}
schema('netdisco')->resultset('DeviceIp')
->search({}, { columns => [qw/ip alias/] })->all;
my %devs = ( map {($_->ip => $_->dns)} $devices->all );
my %locs = ( map {($_->ip => $_->location)} $devices->all );
# Check for no topology info
unless ($links->count > 0) {
debug "make_graph() - No topology information. skipping.";
return undef;
}
my %link_seen = ();
my %linkmap = ();
while (my $link = $links->next) {
my $source = $link->ip;
my $dest = $link->remote_ip;
my $speed = $link->speed;
my $type = $link->remote_type;
# Check for Aliases
if (defined $aliases{$dest}) {
# Set to root device
$dest = $aliases{$dest};
}
# Remove loopback - After alias check (bbaetz)
if ($source eq $dest) {
debug " make_graph() - Loopback on $source";
next;
}
# Skip IP Phones
if (defined $type and $type =~ /ip.phone/i) {
debug " make_graph() - Skipping IP Phone. $source -> $dest ($type)";
next;
}
next if exists $link_seen{$source}->{$dest};
push(@{ $linkmap{$source} }, $dest);
# take care of reverse too
$link_seen{$source}->{$dest}++;
$link_seen{$dest}->{$source}++;
$GRAPH_SPEED{$source}->{$dest}->{speed}=$speed;
$GRAPH_SPEED{$dest}->{$source}->{speed}=$speed;
}
foreach my $link (keys %linkmap) {
foreach my $dest (@{ $linkmap{$link} }) {
foreach my $side ($link, $dest) {
unless (defined $GRAPH{$side}) {
my $is_dev = exists $devs{$side};
my $dns = $is_dev ?
$devs{$side} :
hostname_from_ip($side);
# Default to IP if no dns
$dns = defined $dns ? $dns : "($side)";
$G->add_vertex($side);
debug " make_graph() - add_vertex('$side')";
$GRAPH{$side}->{dns} = $dns;
$GRAPH{$side}->{isdev} = $is_dev;
$GRAPH{$side}->{seen}++;
$GRAPH{$side}->{location} = $locs{$side};
}
}
$G->add_edge($link,$dest);
debug " make_graph - add_edge('$link','$dest')";
}
}
return $G;
}
sub _homepath {
my ($path, $default) = @_;
my $home = $ENV{NETDISCO_HOME};
my $item = setting('graph')->{$path} || $default;
return undef unless defined($item);
if ($item =~ m,^/,) {
return $item;
}
else {
$home =~ s,/*$,,;
return $home . "/" . $item;
}
}
1;

View File

@@ -37,7 +37,7 @@ database storage.
Returns false, and might log a debug level message, if the checks fail. Returns false, and might log a debug level message, if the checks fail.
Returns a true value if these checks pass: Returns a true value (the MAC address in IEEE format) if these checks pass:
=over 4 =over 4
@@ -67,12 +67,13 @@ MAC address does not belong to an interface on any known Device
sub check_mac { sub check_mac {
my ($device, $node, $port_macs) = @_; my ($device, $node, $port_macs) = @_;
my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0); my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0);
my $devip = (ref $device ? $device->ip : '');
$port_macs ||= {}; $port_macs ||= {};
# incomplete MAC addresses (BayRS frame relay DLCI, etc) # incomplete MAC addresses (BayRS frame relay DLCI, etc)
if ($mac->get_error) { if ($mac->get_error) {
debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping', debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping',
$device->ip, $node; $devip, $node;
return 0; return 0;
} }
else { else {
@@ -92,32 +93,32 @@ sub check_mac {
# multicast # multicast
if ($node =~ m/^[0-9a-f](?:1|3|5|7|9|b|d|f):/) { if ($node =~ m/^[0-9a-f](?:1|3|5|7|9|b|d|f):/) {
debug sprintf ' [%s] check_mac - multicast mac [%s] - skipping', debug sprintf ' [%s] check_mac - multicast mac [%s] - skipping',
$device->ip, $node; $devip, $node;
return 0; return 0;
} }
# VRRP # VRRP
if (index($node, '00:00:5e:00:01:') == 0) { if (index($node, '00:00:5e:00:01:') == 0) {
debug sprintf ' [%s] check_mac - VRRP mac [%s] - skipping', debug sprintf ' [%s] check_mac - VRRP mac [%s] - skipping',
$device->ip, $node; $devip, $node;
return 0; return 0;
} }
# HSRP # HSRP
if (index($node, '00:00:0c:07:ac:') == 0) { if (index($node, '00:00:0c:07:ac:') == 0) {
debug sprintf ' [%s] check_mac - HSRP mac [%s] - skipping', debug sprintf ' [%s] check_mac - HSRP mac [%s] - skipping',
$device->ip, $node; $devip, $node;
return 0; return 0;
} }
# device's own MACs # device's own MACs
if (exists $port_macs->{$node}) { if ($port_macs and exists $port_macs->{$node}) {
debug sprintf ' [%s] check_mac - mac [%s] is device port - skipping', debug sprintf ' [%s] check_mac - mac [%s] is device port - skipping',
$device->ip, $node; $devip, $node;
return 0; return 0;
} }
return 1; return $node;
} }
=head2 check_node_no( $ip, $setting_name ) =head2 check_node_no( $ip, $setting_name )
@@ -221,11 +222,9 @@ Returns false if the host is not permitted to nbtstat the target node.
sub is_nbtstatable { sub is_nbtstatable {
my $ip = shift; my $ip = shift;
return _bail_msg("is_nbtstatable: node matched nbtstat_no") return if check_node_no($ip, 'nbtstat_no');
if check_node_no($ip, 'nbtstat_no');
return _bail_msg("is_nbtstatable: node failed to match nbtstat_only") return unless check_node_only($ip, 'nbtstat_only');
unless check_node_only($ip, 'nbtstat_only');
return 1; return 1;
} }

View File

@@ -0,0 +1,52 @@
package App::Netdisco::Util::NodeMonitor;
use App::Netdisco;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::DNS qw/hostname_from_ip ipv4_from_hostname/;
use base 'Exporter';
our @EXPORT_OK = qw/
monitor
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
sub _email {
my ($to, $subject, $body) = @_;
my $domain = setting('domain_suffix') || 'localhost';
$domain =~ s/^\.//;
my $SENDMAIL = '/usr/sbin/sendmail';
open (SENDMAIL, "| $SENDMAIL -t") or die "Can't open sendmail at $SENDMAIL.\n";
print SENDMAIL "To: $to\n";
print SENDMAIL "From: Netdisco <netdisco\@$domain>\n";
print SENDMAIL "Subject: $subject\n\n";
print SENDMAIL $body;
close (SENDMAIL) or die "Can't send letter. $!\n";
}
sub monitor {
my $monitor = schema('netdisco')->resultset('Virtual::NodeMonitor');
while (my $entry = $monitor->next) {
my $body = <<"end_body";
........ n e t d i s c o .........
Node : @{[$entry->mac]} (@{[$entry->why]})
When : @{[$entry->date]}
Switch : @{[$entry->name]} (@{[$entry->switch]})
Port : @{[$entry->port]} (@{[$entry->portname]})
Location: @{[$entry->location]}
end_body
_email(
$entry->cc,
"Saw mac @{[$entry->mac]} (@{[$entry->why]}) on @{[$entry->name]} @{[$entry->port]}",
$body
);
}
}
1;

View File

@@ -68,6 +68,10 @@ sub vlan_reconfig_check {
=item * =item *
Permission check that C<portctl_nameonly> is false in Netdisco config.
=item *
Permission check that C<portctl_uplinks> is true in Netdisco config, if Permission check that C<portctl_uplinks> is true in Netdisco config, if
C<$port> is an uplink. C<$port> is an uplink.
@@ -95,6 +99,10 @@ sub port_reconfig_check {
my $has_phone = port_has_phone($port); my $has_phone = port_has_phone($port);
my $is_vlan = is_vlan_interface($port); my $is_vlan = is_vlan_interface($port);
# only permitted to change interface name
return "forbidden: not permitted to change port configuration"
if setting('portctl_nameonly');
# uplink check # uplink check
return "forbidden: port [$name] on [$ip] is an uplink" return "forbidden: port [$name] on [$ip] is an uplink"
if $port->remote_type and not $has_phone and not setting('portctl_uplinks'); if $port->remote_type and not $has_phone and not setting('portctl_uplinks');

View File

@@ -0,0 +1,75 @@
package App::Netdisco::Web::Plugin::AdminTask::NodeMonitor;
use Dancer ':syntax';
use Dancer::Plugin::Ajax;
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;
use App::Netdisco::Web::Plugin;
use App::Netdisco::Util::Node 'check_mac';
register_admin_task({
tag => 'nodemonitor',
label => 'Node Monitor',
});
sub _sanity_ok {
return 0 unless param('mac')
and check_mac(undef, param('mac'));
params->{mac} = check_mac(undef, param('mac'));
return 1;
}
ajax '/ajax/control/admin/nodemonitor/add' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $monitor = schema('netdisco')->resultset('NodeMonitor')
->create({
mac => param('mac'),
active => (param('active') ? \'true' : \'false'),
why => param('why'),
cc => param('cc'),
});
});
};
ajax '/ajax/control/admin/nodemonitor/del' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
schema('netdisco')->resultset('NodeMonitor')
->find({mac => param('mac')})->delete;
});
};
ajax '/ajax/control/admin/nodemonitor/update' => require_role admin => sub {
send_error('Bad Request', 400) unless _sanity_ok();
schema('netdisco')->txn_do(sub {
my $monitor = schema('netdisco')->resultset('NodeMonitor')
->find({mac => param('mac')});
return unless $monitor;
$monitor->update({
mac => param('mac'),
active => (param('active') ? \'true' : \'false'),
why => param('why'),
cc => param('cc'),
date => \'now()',
});
});
};
ajax '/ajax/content/admin/nodemonitor' => require_role admin => sub {
my $set = schema('netdisco')->resultset('NodeMonitor')
->search(undef, { order_by => [qw/active date mac/] });
content_type('text/html');
template 'ajax/admintask/nodemonitor.tt', {
results => $set,
}, { layout => undef };
};
true;

View File

@@ -119,6 +119,8 @@ arpnip_min_age: 0
nbtstat_no: [] nbtstat_no: []
nbtstat_only: [] nbtstat_only: []
nbtstat_max_age: 7 nbtstat_max_age: 7
nbtstat_interval: 0.02
nbtstat_timeout: 1
expire_devices: 0 expire_devices: 0
expire_nodes: 0 expire_nodes: 0
expire_nodes_archive: 0 expire_nodes_archive: 0
@@ -144,6 +146,7 @@ ignore_interfaces:
ignore_private_nets: false ignore_private_nets: false
reverse_sysname: false reverse_sysname: false
vlanctl: true vlanctl: true
portctl_nameonly: false
portctl_nophones: false portctl_nophones: false
portctl_vlans: false portctl_vlans: false
portctl_uplinks: false portctl_uplinks: false
@@ -165,7 +168,7 @@ port_control_reasons:
# -------------- # --------------
workers: workers:
interactives: 2 interactives: 1
pollers: 10 pollers: 10
sleep_time: 2 sleep_time: 2
queue: PostgreSQL queue: PostgreSQL
@@ -210,6 +213,50 @@ job_type_keys:
Poller: pollers Poller: pollers
Interactive: interactives Interactive: interactives
# ---------------
# GraphViz Export
# ---------------
graph:
# ---- Graph Settings ----
edge_color : wheat
graph : 'graph/netmap.gif'
graph_png : 'graph/netmap.png'
graph_bg : black
graph_clusters : false # try fdp layout
graph_color : white
graph_default : png
#graph_dir : net_dir.gif
graph_epsilon : 6
graph_layout : twopi # try neato or fdp too
graph_map : 'graph/netmap.map'
graph_overlap : scale
graph_nodesep : 2
graph_ranksep : .3
graph_raw : 'graph/graph_raw.dot'
graph_splines : false
graph_svg : 'graph/netmap.svg'
graph_timeout : 90
graph_x : 30
graph_y : 30
node_fillcolor : dimgrey
node_font : lucon
node_fontsize : 46.0
node_fontcolor : white
node_problem : red
node_shape : box
node_style : filled
#edge_style : setlinewidth(10)
# ---- Node Maps ----
# variable:matching pattern:node attribute:attribute value:key:key name
#node_map:
# - 'label:cat(?!-g):fillcolor:blue:cat:Blue Box - Catalyst Device'
# - 'label:-g:fillcolor:darkgreen:dev-g:Green Box - Gateway / Router'
# - 'ip:^192.168\.:color:yellow:dev:Yellow Border - ResNet'
# --------------- # ---------------
# DANCER INTERNAL # DANCER INTERNAL
# --------------- # ---------------

View File

@@ -0,0 +1,92 @@
<table id="data-table" class="table table-striped table-bordered" width="100%" cellspacing="0">
<thead>
<tr>
<th class="nd_center-cell">Date Added</th>
<th class="nd_center-cell">MAC Address</th>
<th class="nd_center-cell">Enabled</th>
<th class="nd_center-cell">Reason</th>
<th class="nd_center-cell">Email</th>
<th class="nd_center-cell">Action</th>
</tr>
</thead>
</tbody>
<tr>
<td class="nd_center-cell"></td>
<td class="nd_center-cell"><input data-form="add" name="mac" type="text"></td>
<td class="nd_center-cell"><input data-form="add" name="active" type="checkbox" checked></td>
<td class="nd_center-cell"><input data-form="add" name="why" type="text"></td>
<td class="nd_center-cell"><input data-form="add" name="cc" type="email"></td>
<td class="nd_center-cell">
<button class="btn btn-small nd_adminbutton" name="add" type="submit"><i class="icon-plus-sign"></i> Add</button>
</td>
</tr>
[% SET count = 0 %]
[% WHILE (row = results.next) %]
[% SET count = count + 1 %]
<tr>
<td class="nd_center-cell">[% row.date | html_entity %]</td>
<td class="nd_center-cell">
<input data-form="update" name="mac" type="text" value="[% row.mac | html_entity %]">
</td>
<td class="nd_center-cell">
<input data-form="update" name="active" type="checkbox" [% 'checked="checked"' IF row.active %]>
</td>
<td class="nd_center-cell">
<input data-form="update" name="why" type="text" value="[% row.why | html_entity %]">
</td>
<td class="nd_center-cell">
<input data-form="update" name="cc" type="email" value="[% row.cc | html_entity %]">
</td>
<td class="nd_center-cell">
<button class="btn nd_adminbutton" name="update" type="submit"><i class="icon-save text-warning"></i></button>
<button class="btn" data-toggle="modal"
data-target="#nd_devdel-[% count %]" type="button"><i class="icon-trash text-error"></i></button>
<div id="nd_devdel-[% count %]" class="nd_modal nd_deep-horizon modal hide fade" tabindex="-1"
role="dialog" aria-labelledby="nd_devdel-label-[% count %]" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
<h3 id="nd_devdel-label-[% count %]">Are you sure?</h3>
</div>
<div class="modal-body">
<blockquote>
<p class="text-info">Monitor for &quot;[% row.mac | html_entity %]&quot; will be removed.</p>
</blockquote>
<input data-form="del" name="mac" type="hidden" value="[% row.mac | html_entity %]">
</div>
<div class="modal-footer">
<button class="btn btn-success" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button class="btn btn-danger nd_adminbutton" name="del" data-dismiss="modal">Confirm</button>
</div>
</div>
</td>
</tr>
[% END %]
</tbody>
</table>
<script>
$(document).ready(function() {
$('#data-table').dataTable({
"stateSave": true,
"pageLength": [% settings.table_pagesize %],
"language": {
"search": 'Filter records: '
},
"columnDefs": [
{
"targets": [ 0, 2, 5 ],
"searchable": false
},
{
"targets": [ 0, 2, 5 ],
"orderable": false
}
]
} );
} );
</script>

View File

@@ -25,7 +25,7 @@ $(document).ready(function() {
}, },
"serverSide": true, "serverSide": true,
"order": [[ 0, "desc" ]], "order": [[ 0, "desc" ]],
"ajax": "/ajax/control/admin/userlog/data", "ajax": "[% uri_for('/ajax/control/admin/userlog/data') %]",
"columns": [{ "columns": [{
"data": 'creation', "data": 'creation',
"className": "nd_center-cell", "className": "nd_center-cell",

View File

@@ -60,12 +60,14 @@
[% IF user_can_port_control AND params.c_admin %] [% IF user_can_port_control AND params.c_admin %]
[% IF row.up_admin == 'up' %] [% IF row.up_admin == 'up' %]
<td nowrap class="nd_editable-cell" data-action="down" <td nowrap class="nd_editable-cell" data-action="down"
data-order="[% row.port | html_entity %]" data-filter="[% row.port | html_entity %]"
data-field="c_port" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]"> 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" <i class="icon-hand-down nd_hand-icon"
rel="tooltip" data-placement="top" data-offset="3" rel="tooltip" data-placement="top" data-offset="3"
data-animation="" data-title="Click to Disable"></i> data-animation="" data-title="Click to Disable"></i>
[% ELSE %] [% ELSE %]
<td nowrap class="nd_editable-cell" data-action="up" <td nowrap class="nd_editable-cell" data-action="up"
data-order="[% row.port | html_entity %]" data-filter="[% row.port | html_entity %]"
data-field="c_port" data-for-device="[% device.ip | html_entity %]" data-for-port="[% row.port | html_entity %]"> 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" <i class="icon-hand-up nd_hand-icon"
rel="tooltip" data-placement="top" data-offset="3" rel="tooltip" data-placement="top" data-offset="3"
@@ -78,7 +80,7 @@
data-animation="" data-title="View Port Log"></i> data-animation="" data-title="View Port Log"></i>
</a> </a>
[% ELSE %] [% ELSE %]
<td nowrap> <td nowrap data-order="[% row.port | html_entity %]" data-filter="[% row.port | html_entity %]">
[% END %] [% END %]
<a class="nd_this-port-only nd_port-only-first" href="[% uri_for('/device', <a class="nd_this-port-only nd_port-only-first" href="[% uri_for('/device',
self_options) %]&q=[% params.q | uri %]&f=[% row.port | uri %]&prefer=port"> self_options) %]&q=[% params.q | uri %]&f=[% row.port | uri %]&prefer=port">

View File

@@ -45,7 +45,7 @@ $(document).ready(function() {
"search": 'Filter records: ' "search": 'Filter records: '
}, },
"serverSide": true, "serverSide": true,
"ajax": '/ajax/content/report/apradiochannelpower/data', "ajax": "[% uri_for('/ajax/content/report/apradiochannelpower/data') %]",
"order": [[ 0, 'asc' ]], "order": [[ 0, 'asc' ]],
"columns": [ "columns": [
{ {

View File

@@ -25,7 +25,7 @@ $(document).ready(function() {
{ {
"data": 'ip', "data": 'ip',
"render": function(data, type, row, meta) { "render": function(data, type, row, meta) {
return '<a search_device %]&q=' + encodeURIComponent(data) + '">' + he.encode(row.ip || row.name) + '</a>'; return '<a href="[% search_device %]&q=' + encodeURIComponent(data) + '">' + he.encode(row.name || row.ip) + '</a>';
} }
}, { }, {
"data": 'dns', "data": 'dns',

View File

@@ -71,7 +71,7 @@ $(document).ready(function() {
"search": 'Filter records: ' "search": 'Filter records: '
}, },
"serverSide": true, "serverSide": true,
"ajax": '/ajax/content/report/devicepoestatus/data', "ajax": "[% uri_for('/ajax/content/report/devicepoestatus/data') %]",
"order": [[ 0, 'asc' ]], "order": [[ 0, 'asc' ]],
"columns": [ "columns": [
{ {

View File

@@ -25,11 +25,15 @@ $(document).ready(function() {
"data": 'ip', "data": 'ip',
"render": function(data, type, row, meta) { "render": function(data, type, row, meta) {
var cell_str = he.encode(data); var cell_str = he.encode(data);
if (row.time_last && row.node) { if (type == 'display') {
cell_str = '<a href="[% search_node %]&q=' + encodeURIComponent(data) + (row.active ? '' : '&archived=on') + '">' + he.encode(data) + (row.active ? '' : '&nbsp;<i class="icon-book text-warning"></i>&nbsp;') + '</a>'; if (row.time_last && row.node) {
} cell_str = '<a href="[% search_node %]&q=' + encodeURIComponent(data)
else if (row.time_last) { + (row.active ? '' : '&archived=on') + '">' + he.encode(data)
cell_str = '<a href="[% search_device %]&q=' + encodeURIComponent(data) + '">' + he.encode(data) + '</a>'; + (row.active ? '' : '&nbsp;<i class="icon-book text-warning"></i>&nbsp;') + '</a>';
}
else if (row.time_last) {
cell_str = '<a href="[% search_device %]&q=' + encodeURIComponent(data) + '">' + he.encode(data) + '</a>';
}
} }
return cell_str; return cell_str;
} }

View File

@@ -40,7 +40,7 @@ $(document).ready(function() {
"serverSide": true, "serverSide": true,
"searching": false, "searching": false,
"order": [[ 0, "desc" ]], "order": [[ 0, "desc" ]],
"ajax": '/ajax/content/report/moduleinventory/data?[% url(params('query').hash) %]', "ajax": "[% uri_for('/ajax/content/report/moduleinventory/data') %]?[% url(params('query').hash) %]",
"columns": [ "columns": [
{ {
"data": 'ip', "data": 'ip',

View File

@@ -35,7 +35,7 @@ $(document).ready(function() {
[% IF opt %] [% IF opt %]
"serverSide": true, "serverSide": true,
"order": [[ 0, "desc" ]], "order": [[ 0, "desc" ]],
"ajax": '/ajax/content/report/netbios/data?[% url(params('query').hash) %]', "ajax": "[% uri_for('/ajax/content/report/netbios/data') %]?[% url(params('query').hash) %]",
"columns": [ "columns": [
{ {
"data": 'domain', "data": 'domain',

View File

@@ -35,7 +35,7 @@ $(document).ready(function() {
[% IF opt %] [% IF opt %]
"serverSide": true, "serverSide": true,
"order": [[ 0, "desc" ]], "order": [[ 0, "desc" ]],
"ajax": '/ajax/content/report/nodevendor/data?[% url(params('query').hash) %]', "ajax": "[% uri_for('/ajax/content/report/nodevendor/data') %]?[% url(params('query').hash) %]",
"columns": [ "columns": [
{ {
"data": 'mac', "data": 'mac',

View File

@@ -42,7 +42,7 @@ $(document).ready(function() {
}, { }, {
"data": 'port_vlans.vlan', "data": 'port_vlans.vlan',
"render": function(data, type, row, meta) { "render": function(data, type, row, meta) {
return data; return data || '';
} }
} }
] ]

View File

@@ -13,17 +13,21 @@
</thead> </thead>
<tbody> <tbody>
[% FOREACH platform IN models.all %] [% FOREACH platform IN models.all %]
[% NEXT UNLESS platform.vendor AND platform.model %] [% NEXT UNLESS platform.vendor OR platform.model %]
<tr> <tr>
<th> <th>
[% IF platform.vendor %]
<a class="nd_linkcell" <a class="nd_linkcell"
href="[% search_device %]&q=[% platform.vendor | uri %]&vendor=[% platform.vendor | uri %]"> href="[% search_device %]&q=[% platform.vendor | uri %]&vendor=[% platform.vendor | uri %]">
[% platform.vendor | html_entity %]</a> [% platform.vendor | html_entity %]</a>
[% ELSE %]unknown[% END %]
</th> </th>
<th> <th>
[% IF platform.model %]
<a class="nd_linkcell" <a class="nd_linkcell"
href="[% search_device %]&q=[% platform.model | uri %]&model=[% platform.model | uri %]"> href="[% search_device %]&q=[% platform.model | uri %]&model=[% platform.model | uri %]">
[% platform.model | html_entity %]</a> [% platform.model | html_entity %]</a>
[% ELSE %]unknown[% END %]
</th> </th>
<th>[% platform.get_column('count') | html_entity %]</th> <th>[% platform.get_column('count') | html_entity %]</th>
</tr> </tr>
@@ -43,13 +47,15 @@
</thead> </thead>
<tbody> <tbody>
[% FOREACH release IN releases.all %] [% FOREACH release IN releases.all %]
[% NEXT UNLESS (release.os AND release.os_ver) %] [% NEXT UNLESS (release.os OR release.os_ver) %]
<tr> <tr>
<th>[% release.os | html_entity %]</th> <th>[% release.os || 'unknown' | html_entity %]</th>
<th> <th>
[% IF release.os_ver %]
<a class="nd_linkcell" <a class="nd_linkcell"
href="[% search_device %]&q=[% release.os_ver | uri %]&os_ver=[% release.os_ver | uri %]"> href="[% search_device %]&q=[% release.os_ver | uri %]&os_ver=[% release.os_ver | uri %]">
[% release.os_ver | html_entity %]</a> [% release.os_ver | html_entity %]</a>
[% ELSE %]unknown[% END %]
</th> </th>
<th>[% release.get_column('count') | html_entity %]</th> <th>[% release.get_column('count') | html_entity %]</th>
</tr> </tr>

View File

@@ -1,3 +1,7 @@
2.004000 - 2014-07-13
* Added by_hostname config option support
2.003002 - 2014-05-03 2.003002 - 2014-05-03
* POD fixups * POD fixups

View File

@@ -24,4 +24,4 @@ resources:
homepage: http://netdisco.org/ homepage: http://netdisco.org/
license: http://opensource.org/licenses/bsd-license.php license: http://opensource.org/licenses/bsd-license.php
repository: git://git.code.sf.net/p/netdisco/netdisco-ng repository: git://git.code.sf.net/p/netdisco/netdisco-ng
version: 2.003002 version: 2.004000

View File

@@ -1,6 +1,6 @@
package App::NetdiscoX::Web::Plugin::RANCID; package App::NetdiscoX::Web::Plugin::RANCID;
our $VERSION = '2.003002'; our $VERSION = '2.004000';
use Dancer ':syntax'; use Dancer ':syntax';
@@ -25,6 +25,7 @@ hook 'before_template' => sub {
my $config = config; my $config = config;
my $tokens = shift; my $tokens = shift;
my $device = $tokens->{d}; my $device = $tokens->{d};
my $domain_suffix = setting('domain_suffix') || '';
# defaults # defaults
$tokens->{rancidgroup} = ''; $tokens->{rancidgroup} = '';
@@ -33,14 +34,17 @@ hook 'before_template' => sub {
return unless exists $config->{rancid}; return unless exists $config->{rancid};
my $rancid = $config->{rancid}; my $rancid = $config->{rancid};
$rancid->{groups} ||= {}; $rancid->{groups} ||= {};
$rancid->{by_ip} ||= []; $rancid->{by_ip} ||= [];
$rancid->{by_hostname} ||= [];
foreach my $g (keys %{ $rancid->{groups} }) { foreach my $g (keys %{ $rancid->{groups} }) {
if (check_acl( get_device($device->{ip}), $rancid->{groups}->{$g} )) { if (check_acl( get_device($device->{ip}), $rancid->{groups}->{$g} )) {
$tokens->{rancidgroup} = $g; $tokens->{rancidgroup} = $g;
$tokens->{ranciddevice} = $device->{ip} $tokens->{ranciddevice} = $device->{ip}
if 0 < scalar grep {$_ eq $g} @{ $rancid->{by_ip} }; if 0 < scalar grep {$_ eq $g} @{ $rancid->{by_ip} };
$tokens->{ranciddevice} =~ s/$domain_suffix$//
if 0 < scalar grep {$_ eq $g} @{ $rancid->{by_hostname} };
last; last;
} }
} }
@@ -89,7 +93,8 @@ known to Netdisco. This uses the same configuration as for
L<netdisco-rancid-export>, an example of which is below: L<netdisco-rancid-export>, an example of which is below:
rancid: rancid:
by_ip: [ other ] by_ip: [ other ]
by_hostname: [ other2 ]
groups: groups:
switch: [ 'name:.*[Ss][Ww].*' ] switch: [ 'name:.*[Ss][Ww].*' ]
rtr: [ 'name:[rR]tr.*' ] rtr: [ 'name:[rR]tr.*' ]
@@ -102,7 +107,8 @@ regular expression as in the above example.
The device DNS name is used, or if missing the device SNMP sysName. Adding the The device DNS name is used, or if missing the device SNMP sysName. Adding the
group to the list in C<by_ip> will make the link include the device IP group to the list in C<by_ip> will make the link include the device IP
instead of the name. instead of the name. Adding the group to the list in C<by_hostname> will
use the device FQDN minus the C<domain_suffix> config item (i.e. the hostname).
=head2 open_in_same_window =head2 open_in_same_window