Node search by NetBIOS name. Update documentation.

This commit is contained in:
Eric A. Miller
2014-02-09 00:27:23 -05:00
parent 7a977f0ddd
commit 1a47920cfb
8 changed files with 415 additions and 45 deletions

View File

@@ -5,8 +5,9 @@
* [#86] Use Vendor abbrevs to enhance node display in device port view
* [#74] Device Name / DNS mismatches report
* [#71] Node search by date (but not time)
* [#73] NetBIOS Poller (nbtstat and nbtwalk), NetBIOS Node Report,
and provide information when available in Node and Port views
* [#73] NetBIOS Poller (nbtstat and nbtwalk), Node search by NetBIOS name,
NetBIOS Node Report, and provide information when available in Node
and Port views
* [#56] Support API call to /login
[ENHANCEMENTS]

View File

@@ -312,17 +312,13 @@ See L<App::Netdisco::Web::Plugin> for further information.
Lots of information about the architecture of this application is contained
within the L<Developer|App::Netdisco::Manual::Developing> documentation.
=head1 Caveats
NetBIOS Node properies are not yet shown.
=head1 AUTHOR
Oliver Gorwits <oliver@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2012, 2013 by The Netdisco Developer Team.
This software is copyright (c) 2012, 2013, 2014 by The Netdisco Developer Team.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

View File

@@ -98,6 +98,17 @@ See also the C<node_sightings> helper routine, below.
__PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node',
{ 'foreign.mac' => 'self.mac' } );
=head2 netbios
Returns the set of C<node_nbt> entries associated with the MAC of this IP.
That is, all the NetBIOS entries recorded which shared the same MAC with this
IP Address.
=cut
__PACKAGE__->has_many( netbios => 'App::Netdisco::DB::Result::NodeNbt',
{ 'foreign.mac' => 'self.mac' } );
my $search_attr = {
order_by => {'-desc' => 'time_last'},
'+columns' => {

View File

@@ -45,6 +45,141 @@ __PACKAGE__->set_primary_key("mac");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:XFpxaGAWE13iizQIuVOP3g
=head1 RELATIONSHIPS
=head2 oui
Returns the C<oui> table entry matching this Node. You can then join on this
relation and retrieve the Company name from the related table.
The JOIN is of type LEFT, in case the OUI table has not been populated.
=cut
__PACKAGE__->belongs_to( oui => 'App::Netdisco::DB::Result::Oui',
sub {
my $args = shift;
return {
"$args->{foreign_alias}.oui" =>
{ '=' => \"substring(cast($args->{self_alias}.mac as varchar) for 8)" }
};
},
{ join_type => 'LEFT' }
);
=head2 nodes
Returns the set of C<node> entries associated with this IP. That is, all the
MAC addresses recorded which have ever hosted this IP Address.
Remember you can pass a filter to this method to find only active or inactive
nodes, but do take into account that both the C<node> and C<node_nbt> tables
include independent C<active> fields.
See also the C<node_sightings> helper routine, below.
=cut
__PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node',
{ 'foreign.mac' => 'self.mac' } );
=head2 nodeips
Returns the set of C<node_ip> entries associated with this NetBIOS entry.
That is, the IP addresses which the same MAC address at the time of discovery.
Note that the Active status of the returned IP entries will all be the same
as the current NetBIOS entry.
=cut
__PACKAGE__->has_many( nodeips => 'App::Netdisco::DB::Result::NodeIp',
{ 'foreign.mac' => 'self.mac', 'foreign.active' => 'self.active' } );
my $search_attr = {
order_by => {'-desc' => 'time_last'},
'+columns' => {
time_first_stamp => \"to_char(time_first, 'YYYY-MM-DD HH24:MI')",
time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')",
},
};
=head2 node_sightings( \%cond, \%attrs? )
Returns the set of C<node> entries associated with this IP. That is, all the
MAC addresses recorded which have ever hosted this IP Address.
Remember you can pass a filter to this method to find only active or inactive
nodes, but do take into account that both the C<node> and C<node_ip> tables
include independent C<active> fields.
=over 4
=item *
Results are ordered by time last seen.
=item *
Additional columns C<time_first_stamp> and C<time_last_stamp> provide
preformatted timestamps of the C<time_first> and C<time_last> fields.
=item *
A JOIN is performed on the Device table and the Device DNS column prefetched.
=back
=cut
sub node_sightings {
my ($row, $cond, $attrs) = @_;
return $row
->nodes({}, {
'+columns' => [qw/ device.dns /],
join => 'device',
})
->search_rs({}, $search_attr)
->search($cond, $attrs);
}
=head1 ADDITIONAL COLUMNS
=head2 time_first_stamp
Formatted version of the C<time_first> field, accurate to the minute.
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
between the date stamp and time stamp. That is:
2012-02-06 12:49
=cut
sub time_first_stamp { return (shift)->get_column('time_first_stamp') }
=head2 time_last_stamp
Formatted version of the C<time_last> field, accurate to the minute.
The format is somewhat like ISO 8601 or RFC3339 but without the middle C<T>
between the date stamp and time stamp. That is:
2012-02-06 12:49
=cut
sub time_last_stamp { return (shift)->get_column('time_last_stamp') }
=head2 net_mac
Returns the C<mac> column instantiated into a L<Net::MAC> object.
=cut
sub net_mac { return Net::MAC->new(mac => (shift)->mac) }
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,189 @@
package App::Netdisco::DB::ResultSet::NodeNbt;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings FATAL => 'all';
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
my $search_attr = {
order_by => {'-desc' => 'time_last'},
'+columns' => [
'oui.company',
{ time_first_stamp => \"to_char(time_first, 'YYYY-MM-DD HH24:MI')" },
{ time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')" },
],
join => 'oui'
};
=head1 with_times
This is a modifier for any C<search()> (including the helpers below) which
will add the following additional synthesized columns to the result set:
=over 4
=item time_first_stamp
=item time_last_stamp
=back
=cut
sub with_times {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs({}, $search_attr)
->search($cond, $attrs);
}
=head1 search_by_ip( \%cond, \%attrs? )
my $set = $rs->search_by_ip({ip => '192.0.2.1', active => 1});
Like C<search()>, this returns a ResultSet of matching rows from the
NodeNbt table.
=over 4
=item *
The C<cond> parameter must be a hashref containing a key C<ip> with the value
to search for. Value can either be a simple string of IPv4 or IPv6, or a
L<NetAddr::IP::Lite> object in which case all results within the CIDR/Prefix
will be retrieved.
=item *
Results are ordered by time last seen.
=item *
Additional columns C<time_first_stamp> and C<time_last_stamp> provide
preformatted timestamps of the C<time_first> and C<time_last> fields.
=item *
A JOIN is performed on the OUI table and the OUI C<company> column prefetched.
=back
To limit results only to active IPs, set C<< {active => 1} >> in C<cond>.
=cut
sub search_by_ip {
my ($rs, $cond, $attrs) = @_;
die "ip address required for search_by_ip\n"
if ref {} ne ref $cond or !exists $cond->{ip};
# handle either plain text IP or NetAddr::IP (/32 or CIDR)
my ($op, $ip) = ('=', delete $cond->{ip});
if ('NetAddr::IP::Lite' eq ref $ip and $ip->num > 1) {
$op = '<<=';
$ip = $ip->cidr;
}
$cond->{ip} = { $op => $ip };
return $rs
->search_rs({}, $search_attr)
->search($cond, $attrs);
}
=head1 search_by_name( \%cond, \%attrs? )
my $set = $rs->search_by_name({nbname => 'MYNAME', active => 1});
Like C<search()>, this returns a ResultSet of matching rows from the
NodeNbt table.
=over 4
=item *
The C<cond> parameter must be a hashref containing a key C<nbname> with the
value to search for. The value may optionally include SQL wildcard characters.
=item *
Results are ordered by time last seen.
=item *
Additional columns C<time_first_stamp> and C<time_last_stamp> provide
preformatted timestamps of the C<time_first> and C<time_last> fields.
=item *
A JOIN is performed on the OUI table and the OUI C<company> column prefetched.
=back
To limit results only to active IPs, set C<< {active => 1} >> in C<cond>.
=cut
sub search_by_name {
my ($rs, $cond, $attrs) = @_;
die "nbname field required for search_by_name\n"
if ref {} ne ref $cond or !exists $cond->{nbname};
$cond->{nbname} = { '-ilike' => delete $cond->{nbname} };
return $rs
->search_rs({}, $search_attr)
->search($cond, $attrs);
}
=head1 search_by_mac( \%cond, \%attrs? )
my $set = $rs->search_by_mac({mac => '00:11:22:33:44:55', active => 1});
Like C<search()>, this returns a ResultSet of matching rows from the
NodeNbt table.
=over 4
=item *
The C<cond> parameter must be a hashref containing a key C<mac> with the value
to search for.
=item *
Results are ordered by time last seen.
=item *
Additional columns C<time_first_stamp> and C<time_last_stamp> provide
preformatted timestamps of the C<time_first> and C<time_last> fields.
=item *
A JOIN is performed on the OUI table and the OUI C<company> column prefetched.
=back
To limit results only to active IPs, set C<< {active => 1} >> in C<cond>.
=cut
sub search_by_mac {
my ($rs, $cond, $attrs) = @_;
die "mac address required for search_by_mac\n"
if ref {} ne ref $cond or !exists $cond->{mac};
return $rs
->search_rs({}, $search_attr)
->search($cond, $attrs);
}
1;

View File

@@ -49,6 +49,9 @@ ajax '/ajax/content/search/node' => require_login sub {
my $ips = schema('netdisco')->resultset('NodeIp')
->search_by_mac({mac => $mac->as_IEEE, @active, @times});
my $netbios = schema('netdisco')->resultset('NodeNbt')
->search_by_mac({mac => $mac->as_IEEE, @active, @times});
my $ports = schema('netdisco')->resultset('DevicePort')
->search({mac => $mac->as_IEEE});
@@ -62,17 +65,6 @@ ajax '/ajax/content/search/node' => require_login sub {
}
);
my $netbios = schema('netdisco')->resultset('NodeNbt')->search(
{ mac => $mac->as_IEEE },
{ order_by => { '-desc' => 'time_last' },
'+columns' => [
{
time_first_stamp => \"to_char(time_first, 'YYYY-MM-DD HH24:MI')",
time_last_stamp => \"to_char(time_last, 'YYYY-MM-DD HH24:MI')"
}]
}
);
return unless $sightings->has_rows
or $ips->has_rows
or $ports->has_rows
@@ -89,25 +81,33 @@ ajax '/ajax/content/search/node' => require_login sub {
else {
my $set;
my $name = $node;
if (param('partial')) {
$name = "\%$name\%" if $name !~ m/%/;
}
$set = schema('netdisco')->resultset('NodeNbt')
->search_by_name({nbname => $name, @active, @times});
unless ( $set->has_rows ) {
if (my $ip = NetAddr::IP::Lite->new($node)) {
# search_by_ip() will extract cidr notation if necessary
$set = schema('netdisco')->resultset('NodeIp')
->search_by_ip({ip => $ip, @active, @times});
}
else {
if (param('partial')) {
$node = "\%$node\%" if $node !~ m/%/;
}
elsif (setting('domain_suffix')) {
$node .= setting('domain_suffix')
if index($node, setting('domain_suffix')) == -1;
if ($name !~ m/%/ and setting('domain_suffix')) {
$name .= setting('domain_suffix')
if index($name, setting('domain_suffix')) == -1;
}
$set = schema('netdisco')->resultset('NodeIp')
->search_by_dns({dns => $node, @active, @times});
# if the user selects Vendor search opt, then
# we'll try the OUI company name as a fallback
if (not $set->count and param('show_vendor')) {
if (not $set->has_rows and param('show_vendor')) {
$node = param('q');
$set = schema('netdisco')->resultset('NodeIp')
->with_times
@@ -117,7 +117,8 @@ ajax '/ajax/content/search/node' => require_login sub {
);
}
}
return unless $set and $set->count;
return unless $set and $set->has_rows;
}
$set = $set->search_rs({}, { order_by => 'me.mac' });
template 'ajax/search/node_by_ip.tt', {

View File

@@ -1,4 +1,5 @@
[% USE date(format = '%Y-%m-%d %H:%M') %]
[% USE Number.Format %]
<table class="table table-bordered table-hover nd_floatinghead">
<thead>
<tr>
@@ -16,6 +17,24 @@
</thead>
</tbody>
[% WHILE (row = macs.next) %]
[% IF row.nbname %]
<tr>
<td><a class="nd_linkcell"
href="[% search_node %]&q=[% row.net_mac.$mac_format_call | uri %]">
[% row.net_mac.$mac_format_call | html_entity %]</a></td>
[% IF params.show_vendor %]
<td>[% row.oui.company | html_entity %]</td>
[% END %]
<td>NetBIOS</td>
<td class="nd_linkcell">\\<a href="[% uri_for('report/netbios') %]?domain=[% row.domain | uri %]" title="Devices in this Domain">[% row.domain | html %]</a>\<a href="[% search_node %]&q=[% row.nbname | uri %]">[% row.nbname | html_entity %]</a>
<br>[% row.nbuser || '[No User]' | html %]@<a href="[% search_node %]&q=[% row.ip | uri %]">[% row.ip | html_entity %]</a>
</td>
[% IF params.stamps %]
<td>[% row.time_first_stamp | html_entity %]</td>
<td>[% row.time_last_stamp | html_entity %]</td>
[% END %]
</tr>
[% ELSE %]
<tr>
<td><a class="nd_linkcell"
href="[% search_node %]&q=[% row.net_mac.$mac_format_call | uri %]">
@@ -33,6 +52,40 @@
<td>[% row.time_last_stamp | html_entity %]</td>
[% END %]
</tr>
[% END %]
[% FOREACH nbt IN row.netbios %]
<tr>
<td>&nbsp;</td>
[% IF params.show_vendor %]
<td>&nbsp;</td>
[% END %]
<td>NetBIOS</td>
<td class="nd_linkcell">\\<a href="[% uri_for('report/netbios') %]?domain=[% nbt.domain | uri %]" title="Devices in this Domain">[% nbt.domain | html %]</a>\<a href="[% search_node %]&q=[% nbt.nbname | uri %]">[% nbt.nbname | html_entity %]</a>
<br>[% nbt.nbuser || '[No User]' | html %]@<a href="[% search_node %]&q=[% nbt.ip | uri %]">[% nbt.ip | html_entity %]</a>
</td>
[% IF params.stamps %]
<td>[% date.format(nbt.time_first) | html_entity %]</td>
<td>[% date.format(nbt.time_last) | html_entity %]</td>
[% END %]
</tr>
[% END %]
[% FOREACH ni IN row.nodeips %]
<tr>
<td>&nbsp;</td>
[% IF params.show_vendor %]
<td>&nbsp;</td>
[% END %]
<td>IP &rarr; MAC</td>
<td><a href="[% search_node %]&q=[% ni.ip | uri %]">[% ni.ip | html_entity %]</a>
[% '&nbsp;<i class="icon-book text-warning"></i>&nbsp;' IF NOT ni.active %]
[% ' (' _ ni.dns.remove(settings.domain_suffix) _ ')' IF ni.dns %]
</td>
[% IF params.stamps %]
<td>[% date.format(ni.time_first) | html_entity %]</td>
<td>[% date.format(ni.time_last) | html_entity %]</td>
[% END %]
</tr>
[% END %]
[% FOREACH node IN row.node_sightings(archive_filter) %]
<tr>
<td>&nbsp;</td>
@@ -53,22 +106,6 @@
<td>[% node.time_last_stamp | html_entity %]</td>
[% END %]
</tr>
[% FOREACH nbt IN node.netbios %]
<tr>
<td>&nbsp;</td>
[% IF params.show_vendor %]
<td>&nbsp;</td>
[% END %]
<td>NetBIOS</td>
<td class="nd_linkcell">\\<a href="[% uri_for('report/netbios') %]?domain=[% nbt.domain | uri %]" title="Devices in this Domain">[% nbt.domain | html %]</a>\<a href="[% search_node %]&q=[% nbt.nbname | uri %]">[% nbt.nbname | html_entity %]</a>
<br>[% nbt.nbuser || '[No User]' | html %]@<a href="[% search_node %]&q=[% nbt.ip | uri %]">[% nbt.ip | html_entity %]</a>
</td>
[% IF params.stamps %]
<td>[% date.format(nbt.time_first) | html_entity %]</td>
<td>[% date.format(nbt.time_last) | html_entity %]</td>
[% END %]
</tr>
[% END %]
[% FOREACH wlan IN node.wireless %]
<tr>
<td>&nbsp;</td>

View File

@@ -138,8 +138,8 @@
<br>[% nbt.nbuser || '[No User]' | html %]@<a href="[% search_node %]&q=[% nbt.ip | uri %]">[% nbt.ip | html_entity %]</a>
</td>
[% IF params.stamps %]
<td>[% nbt.get_column('time_first_stamp') | html_entity %]</td>
<td>[% nbt.get_column('time_last_stamp') | html_entity %]</td>
<td>[% nbt.time_first_stamp | html_entity %]</td>
<td>[% nbt.time_last_stamp | html_entity %]</td>
[% END %]
</tr>
[% SET first_row = 0 %]