[#73] NetBIOS Poller - nbtstat and nbtwalk
This commit is contained in:
159
Netdisco/lib/App/Netdisco/Core/Nbtstat.pm
Normal file
159
Netdisco/lib/App/Netdisco/Core/Nbtstat.pm
Normal file
@@ -0,0 +1,159 @@
|
||||
package App::Netdisco::Core::Nbtstat;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use App::Netdisco::Util::SanityCheck 'check_mac';
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
use Time::HiRes 'gettimeofday';
|
||||
use Net::NBName;
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/ do_nbtstat store_nbt /;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Core::Nbtstat
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
Helper subroutines to support parts of the Netdisco application.
|
||||
|
||||
There are no default exports, however the C<:all> tag will export all
|
||||
subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 do_nbtstat( $node )
|
||||
|
||||
Connects to node and gets NetBIOS information. Then adds entries to
|
||||
node_nbt table.
|
||||
|
||||
Returns whether a node is answering netbios calls or not.
|
||||
|
||||
=cut
|
||||
|
||||
sub do_nbtstat {
|
||||
my $host = shift;
|
||||
|
||||
my $ip = NetAddr::IP::Lite->new($host) or return;
|
||||
|
||||
unless ( $ip->version() == 4 ) {
|
||||
debug ' nbtstat only supports IPv4, invalid ip %s', $ip->addr;
|
||||
return;
|
||||
}
|
||||
|
||||
my $nb = Net::NBName->new;
|
||||
|
||||
my $ns = $nb->node_status( $ip->addr );
|
||||
|
||||
# Check for NetBIOS Info
|
||||
return unless $ns;
|
||||
|
||||
my $nbname = _filter_nbname( $ip->addr, $ns );
|
||||
|
||||
if ($nbname) {
|
||||
store_nbt($nbname);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
# filter nbt names / information
|
||||
sub _filter_nbname {
|
||||
my $ip = shift;
|
||||
my $node_status = shift;
|
||||
|
||||
my $server = 0;
|
||||
my $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 : '';
|
||||
|
||||
if ( $suffix == 0 and $G eq "GROUP" ) {
|
||||
$domain = $name;
|
||||
}
|
||||
if ( $suffix == 3 and $G eq "UNIQUE" ) {
|
||||
$nbuser = $name;
|
||||
}
|
||||
if ( $suffix == 0 and $G eq "UNIQUE" ) {
|
||||
$nbname = $name unless $name =~ /^IS~/;
|
||||
}
|
||||
if ( $suffix == 32 and $G eq "UNIQUE" ) {
|
||||
$server = 1;
|
||||
}
|
||||
}
|
||||
|
||||
unless ($nbname) {
|
||||
debug ' nbtstat no computer name found for %s', $ip;
|
||||
return;
|
||||
}
|
||||
|
||||
my $mac = $node_status->mac_address || '';
|
||||
|
||||
unless ( check_mac( $ip, $mac ) ) {
|
||||
|
||||
# Just assume it's the last MAC we saw this IP at.
|
||||
my $node_ip = schema('netdisco')->resultset('NodeIp')
|
||||
->single( { ip => $ip, active => \'true' } );
|
||||
|
||||
if ( !defined $node_ip ) {
|
||||
debug ' 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
|
||||
};
|
||||
}
|
||||
|
||||
=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
|
||||
Workgroup C<domain>, whether the Server Service is running C<server>,
|
||||
and the current NetBIOS user C<nbuser>.
|
||||
|
||||
Adds new entry or time stamps matching one.
|
||||
|
||||
Optionally a literal string can be passed in the second argument for the
|
||||
C<time_last> timestamp, otherwise the current timestamp (C<now()>) is used.
|
||||
|
||||
=cut
|
||||
|
||||
sub store_nbt {
|
||||
my ( $hash_ref, $now ) = @_;
|
||||
$now ||= 'now()';
|
||||
|
||||
schema('netdisco')->resultset('NodeNbt')->update_or_create(
|
||||
{ mac => $hash_ref->{'mac'},
|
||||
ip => $hash_ref->{'ip'},
|
||||
nbname => $hash_ref->{'nbname'},
|
||||
domain => $hash_ref->{'domain'},
|
||||
server => $hash_ref->{'server'},
|
||||
nbuser => $hash_ref->{'nbuser'},
|
||||
active => \'true',
|
||||
time_last => \$now,
|
||||
},
|
||||
{ key => 'primary',
|
||||
for => 'update',
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -14,7 +14,7 @@ __PACKAGE__->result_source_instance->view_definition(<<ENDSQL
|
||||
COUNT( device ) AS number, MIN( started ) AS start, MAX( finished ) AS end,
|
||||
justify_interval( extract ( epoch FROM( max( finished ) - min( started ) ) ) * interval '1 second' ) AS elapsed
|
||||
FROM admin
|
||||
WHERE action IN ( 'discover', 'macsuck', 'arpnip' )
|
||||
WHERE action IN ( 'discover', 'macsuck', 'arpnip', 'nbtstat' )
|
||||
GROUP BY action, entered
|
||||
HAVING count( device ) > 1
|
||||
ORDER BY entered DESC, elapsed DESC
|
||||
|
||||
@@ -191,4 +191,28 @@ sub search_by_mac {
|
||||
->search($cond, $attrs);
|
||||
}
|
||||
|
||||
=head2 ip_version( $version )
|
||||
|
||||
my $rset = $rs->ip_version(4);
|
||||
|
||||
This predefined C<search()> returns a ResultSet of matching rows from the
|
||||
NodeIp table of nodes with addresses of the supplied IP version.
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
The C<version> parameter must be an integer either 4 or 6.
|
||||
|
||||
=cut
|
||||
|
||||
sub ip_version {
|
||||
my ( $rs, $version ) = @_;
|
||||
|
||||
die "ip_version input must be either 4 or 6\n"
|
||||
unless $version && ( $version == 4 || $version == 6 );
|
||||
|
||||
return $rs->search_rs( \[ 'family(ip) = ?', $version ] );
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
@@ -22,8 +22,10 @@ sub capacity_for {
|
||||
debug "checking local capacity for action $action";
|
||||
|
||||
my $action_map = {
|
||||
Poller => [qw/discoverall discover arpwalk arpnip macwalk macsuck expiry/],
|
||||
Interactive => [qw/location contact portcontrol portname vlan power/],
|
||||
Poller => [
|
||||
qw/discoverall discover arpwalk arpnip macwalk macsuck nbtstat nbtwalk expiry/
|
||||
],
|
||||
Interactive => [qw/location contact portcontrol portname vlan power/],
|
||||
};
|
||||
|
||||
my $role_map = {
|
||||
|
||||
@@ -13,7 +13,7 @@ my $fqdn = hostfqdn || 'localhost';
|
||||
|
||||
my $role_map = {
|
||||
(map {$_ => 'Poller'}
|
||||
qw/discoverall discover arpwalk arpnip macwalk macsuck expiry/),
|
||||
qw/discoverall discover arpwalk arpnip macwalk macsuck nbtstat nbtwalk expiry/),
|
||||
(map {$_ => 'Interactive'}
|
||||
qw/location contact portcontrol portname vlan power/)
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ with 'App::Netdisco::Daemon::Worker::Common';
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Device',
|
||||
'App::Netdisco::Daemon::Worker::Poller::Arpnip',
|
||||
'App::Netdisco::Daemon::Worker::Poller::Macsuck',
|
||||
'App::Netdisco::Daemon::Worker::Poller::Nbtstat',
|
||||
'App::Netdisco::Daemon::Worker::Poller::Expiry';
|
||||
|
||||
sub worker_type { 'pol' }
|
||||
|
||||
@@ -88,4 +88,80 @@ sub _single_body {
|
||||
return job_done("Ended $job_type for ". $host->addr);
|
||||
}
|
||||
|
||||
# _walk_nodes_body
|
||||
# Queue a job for all active nodes that have been seen in the last
|
||||
# configured days.
|
||||
#
|
||||
sub _walk_nodes_body {
|
||||
my ($self, $job_type) = @_;
|
||||
|
||||
my $action_method = $job_type .'_action';
|
||||
my $job_action = $self->$action_method;
|
||||
|
||||
my $jobqueue = schema('netdisco')->resultset('Admin');
|
||||
my $rs = schema('netdisco')->resultset('NodeIp')
|
||||
->search({ip => { -not_in =>
|
||||
$jobqueue->search({
|
||||
device => { '!=' => undef},
|
||||
action => $job_type,
|
||||
status => { -like => 'queued%' },
|
||||
})->get_column('device')->as_query
|
||||
}, -bool => 'active'});
|
||||
|
||||
my $ip_version = $job_type .'_ip_version';
|
||||
my $job_ip_ver = $self->$ip_version;
|
||||
|
||||
if ($job_ip_ver) {
|
||||
$rs = $rs->ip_version($job_ip_ver)
|
||||
}
|
||||
|
||||
my $config_max_age = $job_type . '_max_age';
|
||||
my $max_age = setting($config_max_age);
|
||||
|
||||
if ($max_age) {
|
||||
my $interval = "$max_age day";
|
||||
$rs = $rs->search(
|
||||
{ time_last => \[ '>= now() - ?::interval', $interval ] } );
|
||||
}
|
||||
|
||||
my @nodes = $rs->get_column('ip')->all;
|
||||
|
||||
my $filter_method = $job_type .'_filter';
|
||||
my $job_filter = $self->$filter_method;
|
||||
|
||||
my @filtered_nodes = grep {$job_filter->($_)} @nodes;
|
||||
|
||||
schema('netdisco')->resultset('Admin')->txn_do_locked(sub {
|
||||
$jobqueue->populate([
|
||||
map {{
|
||||
device => $_,
|
||||
action => $job_type,
|
||||
status => 'queued',
|
||||
}} (@filtered_nodes)
|
||||
]);
|
||||
});
|
||||
|
||||
return job_done("Queued $job_type job for all nodes");
|
||||
}
|
||||
|
||||
sub _single_node_body {
|
||||
my ($self, $job_type, $job) = @_;
|
||||
|
||||
my $action_method = $job_type .'_action';
|
||||
my $job_action = $self->$action_method;
|
||||
|
||||
my $host = NetAddr::IP::Lite->new($job->device);
|
||||
|
||||
my $filter_method = $job_type .'_filter';
|
||||
my $job_filter = $self->$filter_method;
|
||||
|
||||
unless ($job_filter->($host->addr)) {
|
||||
return job_defer("$job_type deferred: $host is not ${job_type}able");
|
||||
}
|
||||
|
||||
$job_action->($host->addr);
|
||||
|
||||
return job_done("Ended $job_type for ". $host->addr);
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
18
Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Nbtstat.pm
Normal file
18
Netdisco/lib/App/Netdisco/Daemon/Worker/Poller/Nbtstat.pm
Normal file
@@ -0,0 +1,18 @@
|
||||
package App::Netdisco::Daemon::Worker::Poller::Nbtstat;
|
||||
|
||||
use App::Netdisco::Core::Nbtstat 'do_nbtstat';
|
||||
use App::Netdisco::Util::Node 'is_nbtstatable';
|
||||
|
||||
use Role::Tiny;
|
||||
use namespace::clean;
|
||||
|
||||
with 'App::Netdisco::Daemon::Worker::Poller::Common';
|
||||
|
||||
sub nbtstat_action { \&do_nbtstat }
|
||||
sub nbtstat_filter { \&is_nbtstatable }
|
||||
sub nbtstat_ip_version { 4 }
|
||||
|
||||
sub nbtwalk { (shift)->_walk_nodes_body('nbtstat', @_) }
|
||||
sub nbtstat { (shift)->_single_node_body('nbtstat', @_) }
|
||||
|
||||
1;
|
||||
@@ -14,10 +14,10 @@ my $jobactions = {
|
||||
discoverall
|
||||
arpwalk
|
||||
macwalk
|
||||
nbtwalk
|
||||
expiry
|
||||
/
|
||||
# saveconfigs
|
||||
# nbtwalk
|
||||
# backup
|
||||
};
|
||||
|
||||
|
||||
@@ -586,6 +586,31 @@ Value: Number. Default: 0.
|
||||
Sets the minimum amount of time in seconds which must elapse between any two
|
||||
arpnip jobs for a device.
|
||||
|
||||
=head3 C<nbtstat_no>
|
||||
|
||||
Value: List of Network Identifiers. Default: Empty List.
|
||||
|
||||
IP addresses in the list will not be visited for nbtstat. You can include
|
||||
hostnames, IP addresses, subnets (nbtstat only supports IPv4), YAML Regexp
|
||||
to match the DNS name, and address ranges (using a hyphen and no whitespace)
|
||||
in the list.
|
||||
|
||||
=head3 C<nbtstat_only>
|
||||
|
||||
Value: List of Network Identifiers. Default: Empty List.
|
||||
|
||||
If present, nbtstat will be limited to IP addresses matching entries in this
|
||||
list. You can include hostnames, IP addresses, subnets
|
||||
(nbtstat only supports IPv4), YAML Regexp to match the DNS name, and address
|
||||
ranges (using a hyphen and no whitespace).
|
||||
|
||||
=head3 C<nbtstat_max_age>
|
||||
|
||||
Value: Number. Default: 7.
|
||||
|
||||
The maximum age of a node in days for it to be checked for NetBIOS
|
||||
information.
|
||||
|
||||
=head3 C<expire_devices>
|
||||
|
||||
Value: Number of Days.
|
||||
@@ -800,6 +825,8 @@ hour fields (which accept same types as C<cron> notation). For example:
|
||||
min: 15
|
||||
hour: '*/2'
|
||||
wday: 'mon-fri'
|
||||
nbtwalk:
|
||||
when: '0 8,13,21 * * *'
|
||||
expiry:
|
||||
when: '20 23 * * *'
|
||||
|
||||
|
||||
@@ -438,9 +438,10 @@ Interactive and Poller workers in their C<config.yml> file (zero or more of
|
||||
each).
|
||||
|
||||
The fourth kind of worker is called the Scheduler and takes care of adding
|
||||
discover, macsuck, and arpnip jobs to the queue (which are in turn handled by
|
||||
the Poller worker). This worker is automatically started only if the user has
|
||||
enabled the "C<housekeeping>" section of their C<deployment.yml> site config.
|
||||
discover, macsuck, arpnip, and nbtstat jobs to the queue (which are in turn
|
||||
handled by the Poller worker). This worker is automatically started only if
|
||||
the user has enabled the "C<housekeeping>" section of their
|
||||
C<deployment.yml> site config.
|
||||
|
||||
=head2 SNMP::Info
|
||||
|
||||
|
||||
206
Netdisco/lib/App/Netdisco/Util/Node.pm
Normal file
206
Netdisco/lib/App/Netdisco/Util/Node.pm
Normal file
@@ -0,0 +1,206 @@
|
||||
package App::Netdisco::Util::Node;
|
||||
|
||||
use Dancer qw/:syntax :script/;
|
||||
use Dancer::Plugin::DBIC 'schema';
|
||||
|
||||
use NetAddr::IP::Lite ':lower';
|
||||
use App::Netdisco::Util::DNS 'hostname_from_ip';
|
||||
|
||||
use base 'Exporter';
|
||||
our @EXPORT = ();
|
||||
our @EXPORT_OK = qw/
|
||||
check_node_acl
|
||||
check_node_no
|
||||
check_node_only
|
||||
is_nbtstatable
|
||||
/;
|
||||
our %EXPORT_TAGS = (all => \@EXPORT_OK);
|
||||
|
||||
=head1 NAME
|
||||
|
||||
App::Netdisco::Util::Node
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
A set of helper subroutines to support parts of the Netdisco application.
|
||||
|
||||
There are no default exports, however the C<:all> tag will export all
|
||||
subroutines.
|
||||
|
||||
=head1 EXPORT_OK
|
||||
|
||||
=head2 check_node_acl( $ip, \@config )
|
||||
|
||||
Given the IP address of a node, returns true if any of the items in C<<
|
||||
\@config >> matches that node, otherwise returns false.
|
||||
|
||||
Normally you use C<check_node_no> and C<check_node_only>, passing the name of the
|
||||
configuration setting to load. This helper instead requires not the name of
|
||||
the setting, but its value.
|
||||
|
||||
=cut
|
||||
|
||||
sub check_node_acl {
|
||||
my ($ip, $config) = @_;
|
||||
my $device = get_device($ip) or return 0;
|
||||
my $addr = NetAddr::IP::Lite->new($device->ip);
|
||||
|
||||
foreach my $item (@$config) {
|
||||
if (ref qr// eq ref $item) {
|
||||
my $name = hostname_from_ip($addr->addr) or next;
|
||||
return 1 if $name =~ $item;
|
||||
next;
|
||||
}
|
||||
|
||||
if ($item =~ m/([a-f0-9]+)-([a-f0-9]+)$/i) {
|
||||
my $first = $1;
|
||||
my $last = $2;
|
||||
|
||||
if ($item =~ m/:/) {
|
||||
next unless $addr->bits == 128;
|
||||
|
||||
$first = hex $first;
|
||||
$last = hex $last;
|
||||
|
||||
(my $header = $item) =~ s/:[^:]+$/:/;
|
||||
foreach my $part ($first .. $last) {
|
||||
my $ip = NetAddr::IP::Lite->new($header . sprintf('%x',$part) . '/128')
|
||||
or next;
|
||||
return 1 if $ip == $addr;
|
||||
}
|
||||
}
|
||||
else {
|
||||
next unless $addr->bits == 32;
|
||||
|
||||
(my $header = $item) =~ s/\.[^.]+$/./;
|
||||
foreach my $part ($first .. $last) {
|
||||
my $ip = NetAddr::IP::Lite->new($header . $part . '/32')
|
||||
or next;
|
||||
return 1 if $ip == $addr;
|
||||
}
|
||||
}
|
||||
|
||||
next;
|
||||
}
|
||||
|
||||
my $ip = NetAddr::IP::Lite->new($item)
|
||||
or next;
|
||||
next unless $ip->bits == $addr->bits;
|
||||
|
||||
return 1 if $ip->contains($addr);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
=head2 check_node_no( $ip, $setting_name )
|
||||
|
||||
Given the IP address of a node, returns true if the configuration setting
|
||||
C<$setting_name> matches that device, else returns false. If the setting
|
||||
is undefined or empty, then C<check_node_no> also returns false.
|
||||
|
||||
print "rejected!" if check_node_no($ip, 'nbtstat_no');
|
||||
|
||||
There are several options for what C<$setting_name> can contain:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
Hostname, IP address, IP prefix
|
||||
|
||||
=item *
|
||||
|
||||
IP address range, using a hyphen and no whitespace
|
||||
|
||||
=item *
|
||||
|
||||
Regular Expression in YAML format which will match the node DNS name, e.g.:
|
||||
|
||||
- !!perl/regexp ^sep0.*$
|
||||
|
||||
=back
|
||||
|
||||
To simply match all nodes, use "C<any>" or IP Prefix "C<0.0.0.0/0>". All
|
||||
regular expressions are anchored (that is, they must match the whole string).
|
||||
To match no nodes we recommend an entry of "C<localhost>" in the setting.
|
||||
|
||||
=cut
|
||||
|
||||
sub check_node_no {
|
||||
my ($ip, $setting_name) = @_;
|
||||
|
||||
my $config = setting($setting_name) || [];
|
||||
return 0 if not scalar @$config;
|
||||
|
||||
return check_acl($ip, $config);
|
||||
}
|
||||
|
||||
=head2 check_node_only( $ip, $setting_name )
|
||||
|
||||
Given the IP address of a node, returns true if the configuration setting
|
||||
C<$setting_name> matches that node, else returns false. If the setting
|
||||
is undefined or empty, then C<check_node_only> also returns true.
|
||||
|
||||
print "rejected!" unless check_node_only($ip, 'nbtstat_only');
|
||||
|
||||
There are several options for what C<$setting_name> can contain:
|
||||
|
||||
=over 4
|
||||
|
||||
=item *
|
||||
|
||||
Hostname, IP address, IP prefix
|
||||
|
||||
=item *
|
||||
|
||||
IP address range, using a hyphen and no whitespace
|
||||
|
||||
=item *
|
||||
|
||||
Regular Expression in YAML format which will match the node DNS name, e.g.:
|
||||
|
||||
- !!perl/regexp ^sep0.*$
|
||||
|
||||
=back
|
||||
|
||||
To simply match all nodes, use "C<any>" or IP Prefix "C<0.0.0.0/0>". All
|
||||
regular expressions are anchored (that is, they must match the whole string).
|
||||
To match no nodes we recommend an entry of "C<localhost>" in the setting.
|
||||
|
||||
=cut
|
||||
|
||||
sub check_node_only {
|
||||
my ($ip, $setting_name) = @_;
|
||||
|
||||
my $config = setting($setting_name) || [];
|
||||
return 1 if not scalar @$config;
|
||||
|
||||
return check_acl($ip, $config);
|
||||
}
|
||||
|
||||
=head2 is_nbtstatable( $ip )
|
||||
|
||||
Given an IP address, returns C<true> if Netdisco on this host is permitted by
|
||||
the local configuration to nbtstat the node.
|
||||
|
||||
The configuration items C<nbtstat_no> and C<nbtstat_only> are checked
|
||||
against the given IP.
|
||||
|
||||
Returns false if the host is not permitted to nbtstat the target node.
|
||||
|
||||
=cut
|
||||
|
||||
sub is_nbtstatable {
|
||||
my $ip = shift;
|
||||
|
||||
return _bail_msg("is_nbtstatable: node matched nbtstat_no")
|
||||
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 1;
|
||||
}
|
||||
|
||||
1;
|
||||
@@ -40,6 +40,7 @@ my %jobs_all = map {$_ => 1} qw/
|
||||
discoverall
|
||||
macwalk
|
||||
arpwalk
|
||||
nbtwalk
|
||||
/;
|
||||
|
||||
foreach my $jobtype (keys %jobs_all, keys %jobs) {
|
||||
|
||||
Reference in New Issue
Block a user