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

@@ -4,7 +4,7 @@ use strict;
use warnings;
use 5.010_000;
our $VERSION = '2.028000';
our $VERSION = '2.028012';
use App::Netdisco::Configuration;
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',
};
# default queue model is Pg
# defaults for workers
setting('workers')->{queue} ||= 'PostgreSQL';
setting('workers')->{interactives} ||= 1;
# force skipped DNS resolution, if unset
setting('dns')->{hosts_file} ||= '/etc/hosts';

View File

@@ -146,6 +146,10 @@ sub store_device {
scalar @aliases, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'};
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.
my $vtpdomains = $snmp->vtp_d_name;
my $vtpdomain;
@@ -682,7 +686,7 @@ sub store_neighbors {
my $remote_ip = $c_ip->{$entry};
my $remote_ipad = NetAddr::IP::Lite->new($remote_ip);
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_cap = $c_cap->{$entry} || [];

View File

@@ -5,11 +5,12 @@ use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Node 'check_mac';
use NetAddr::IP::Lite ':lower';
use Net::NBName;
use App::Netdisco::AnyEvent::Nbtstat;
use Encode;
use base 'Exporter';
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);
=head1 NAME
@@ -25,42 +26,64 @@ subroutines.
=head1 EXPORT_OK
=head2 do_nbtstat( $node )
=head2 nbtstat_resolve_async( $ips )
Connects to node and gets NetBIOS information. Then adds entries to
node_nbt table.
This method uses an asynchronous AnyEvent NetBIOS node status requester
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
sub do_nbtstat {
my ($host, $now) = @_;
my $ip = NetAddr::IP::Lite->new($host) or return;
sub nbtstat_resolve_async {
my $ips = shift;
unless ( $ip->version() == 4 ) {
debug ' nbtstat only supports IPv4, invalid ip %s', $ip->addr;
return;
my $timeout = setting('nbtstat_timeout') || 1;
my $interval = setting('nbtstat_interval') || 0.02;
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;
my $ns = $nb->node_status( $ip->addr );
# Decrement the cv counter to cancel out the send declaration
$cv->end;
# Check for NetBIOS Info
return unless $ns;
# Wait for the resolver to perform all resolutions
$cv->recv;
my $nbname = _filter_nbname( $ip->addr, $ns );
# Close sockets
undef $stater;
if ($nbname) {
store_nbt($nbname, $now);
}
return 1;
return $ips;
}
# filter nbt names / information
sub _filter_nbname {
my $ip = shift;
my $hash_ref = shift;
my $node_status = shift;
my $server = 0;
@@ -68,10 +91,10 @@ sub _filter_nbname {
my $domain = '';
my $nbuser = '';
for my $rr ( $node_status->names ) {
my $suffix = defined $rr->suffix ? $rr->suffix : -1;
my $G = defined $rr->G ? $rr->G : '';
my $name = defined $rr->name ? $rr->name : '';
for my $rr ( @{$node_status->{'names'}} ) {
my $suffix = defined $rr->{'suffix'} ? $rr->{'suffix'} : -1;
my $G = defined $rr->{'G'} ? $rr->{'G'} : '';
my $name = defined $rr->{'name'} ? $rr->{'name'} : '';
if ( $suffix == 0 and $G eq "GROUP" ) {
$domain = $name;
@@ -88,11 +111,11 @@ sub _filter_nbname {
}
unless ($nbname) {
debug ' nbtstat no computer name found for %s', $ip;
debug sprintf ' nbtstat no computer name found for %s', $ip;
return;
}
my $mac = $node_status->mac_address || '';
my $mac = $node_status->{'mac_address'} || '';
unless ( check_mac( $ip, $mac ) ) {
@@ -101,23 +124,23 @@ sub _filter_nbname {
->single( { ip => $ip, -bool => 'active' } );
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;
}
$mac = $node_ip->mac;
}
return {
ip => $ip,
mac => $mac,
nbname => $nbname,
domain => $domain,
server => $server,
nbuser => $nbuser
};
$hash_ref->{'ip'} = $ip;
$hash_ref->{'mac'} = $mac;
$hash_ref->{'nbname'} = Encode::decode('UTF-8', $nbname);
$hash_ref->{'domain'} = Encode::decode('UTF-8', $domain);
$hash_ref->{'server'} = $server;
$hash_ref->{'nbuser'} = Encode::decode('UTF-8', $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
C<mac>, IP C<ip>, Unique NetBIOS Node Name C<nbname>, NetBIOS Domain or

View File

@@ -17,9 +17,8 @@ SELECT *
FROM device
WHERE dns IS NULL
OR name IS NULL
OR lower(trim(TRAILING ?
FROM dns)::text) != lower(trim(TRAILING ?
FROM name)::text)
OR regexp_replace(lower(dns), ? || '$', '')
!= regexp_replace(lower(name), ? || '$', '')
ENDSQL
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")
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');
}
@@ -50,7 +54,7 @@ sub _set_port_generic {
my $ip = $job->device;
my $pn = $job->port;
(my $data = $job->subaction) =~ s/-\w+//;
my $data = $job->subaction;
my $port = get_port($ip, $pn)
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::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::Device qw/get_device is_discoverable/;
use App::Netdisco::Daemon::Util ':all';
@@ -33,7 +33,7 @@ sub nbtstat {
}
# 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({
-bool => 'me.active',
-bool => 'nodes.active',
@@ -46,10 +46,25 @@ sub nbtstat {
})->ip_version(4);
my @nodes = $rs->get_column('ip')->all;
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
$self->_single_node_body('nbtstat', $_, $now)
for @nodes;
# Unless we have IP's don't bother
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);
}

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
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>
Value: Number of Days.
@@ -782,6 +796,14 @@ field to use as the management IP address for a device.
Value: Boolean. Default: C<true>.
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>

View File

@@ -8,6 +8,13 @@ use Net::DNS;
use AnyEvent::DNS;
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.
# this loads+parses /etc/hosts file using AE. dirty hack.
use AnyEvent::Socket 'format_address';
@@ -15,12 +22,10 @@ use AnyEvent::DNS::EtcHosts;
AnyEvent::DNS::EtcHosts::_load_hosts_unless(sub{},AE::cv);
no AnyEvent::DNS::EtcHosts; # unimport
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/
hostname_from_ip hostnames_resolve_async ipv4_from_hostname
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
our %HOSTS = ();
$HOSTS{$_} = [ map { [ $_ ? (format_address $_->[0]) : '' ] }
@{$AnyEvent::DNS::EtcHosts::HOSTS{$_}} ]
for keys %AnyEvent::DNS::EtcHosts::HOSTS;
=head1 NAME
@@ -47,6 +52,13 @@ sub hostname_from_ip {
my $ip = shift;
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 $query = $res->search($ip);
@@ -72,6 +84,12 @@ sub ipv4_from_hostname {
my $name = shift;
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 $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>
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
forward-lookups to verify that the resolved hostnames point to the
address.
will be inserted in the C<dns> key of the hash.
Returns the supplied reference to an array of hashes with dns values for
addresses which resolved.
@@ -103,12 +119,6 @@ addresses which resolved.
sub hostnames_resolve_async {
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
my $done = AE::cv;
@@ -136,6 +146,9 @@ sub hostnames_resolve_async {
# Wait for the resolver to perform all resolutions
$done->recv;
# Remove reference to resolver so that we close sockets
undef $AnyEvent::DNS::RESOLVER if $AnyEvent::DNS::RESOLVER;
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 a true value if these checks pass:
Returns a true value (the MAC address in IEEE format) if these checks pass:
=over 4
@@ -67,12 +67,13 @@ MAC address does not belong to an interface on any known Device
sub check_mac {
my ($device, $node, $port_macs) = @_;
my $mac = Net::MAC->new(mac => $node, 'die' => 0, verbose => 0);
my $devip = (ref $device ? $device->ip : '');
$port_macs ||= {};
# incomplete MAC addresses (BayRS frame relay DLCI, etc)
if ($mac->get_error) {
debug sprintf ' [%s] check_mac - mac [%s] malformed - skipping',
$device->ip, $node;
$devip, $node;
return 0;
}
else {
@@ -92,32 +93,32 @@ sub check_mac {
# 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;
$devip, $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;
$devip, $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;
$devip, $node;
return 0;
}
# 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',
$device->ip, $node;
$devip, $node;
return 0;
}
return 1;
return $node;
}
=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 {
my $ip = shift;
return _bail_msg("is_nbtstatable: node matched nbtstat_no")
if check_node_no($ip, 'nbtstat_no');
return if check_node_no($ip, 'nbtstat_no');
return _bail_msg("is_nbtstatable: node failed to match nbtstat_only")
unless check_node_only($ip, 'nbtstat_only');
return unless check_node_only($ip, 'nbtstat_only');
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 *
Permission check that C<portctl_nameonly> is false in Netdisco config.
=item *
Permission check that C<portctl_uplinks> is true in Netdisco config, if
C<$port> is an uplink.
@@ -95,6 +99,10 @@ sub port_reconfig_check {
my $has_phone = port_has_phone($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
return "forbidden: port [$name] on [$ip] is an uplink"
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;