From 1a47920cfb81217fd5c771447899c777b0bdbfbc Mon Sep 17 00:00:00 2001 From: "Eric A. Miller" Date: Sun, 9 Feb 2014 00:27:23 -0500 Subject: [PATCH] Node search by NetBIOS name. Update documentation. --- Netdisco/Changes | 5 +- Netdisco/lib/App/Netdisco.pm | 6 +- Netdisco/lib/App/Netdisco/DB/Result/NodeIp.pm | 11 + .../lib/App/Netdisco/DB/Result/NodeNbt.pm | 137 ++++++++++++- .../lib/App/Netdisco/DB/ResultSet/NodeNbt.pm | 189 ++++++++++++++++++ .../App/Netdisco/Web/Plugin/Search/Node.pm | 39 ++-- .../share/views/ajax/search/node_by_ip.tt | 69 +++++-- .../share/views/ajax/search/node_by_mac.tt | 4 +- 8 files changed, 415 insertions(+), 45 deletions(-) create mode 100644 Netdisco/lib/App/Netdisco/DB/ResultSet/NodeNbt.pm diff --git a/Netdisco/Changes b/Netdisco/Changes index 02d1e58b..f348af68 100644 --- a/Netdisco/Changes +++ b/Netdisco/Changes @@ -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] diff --git a/Netdisco/lib/App/Netdisco.pm b/Netdisco/lib/App/Netdisco.pm index 74eef98c..74eb6fdd 100644 --- a/Netdisco/lib/App/Netdisco.pm +++ b/Netdisco/lib/App/Netdisco.pm @@ -312,17 +312,13 @@ See L for further information. Lots of information about the architecture of this application is contained within the L documentation. -=head1 Caveats - -NetBIOS Node properies are not yet shown. - =head1 AUTHOR Oliver Gorwits =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: diff --git a/Netdisco/lib/App/Netdisco/DB/Result/NodeIp.pm b/Netdisco/lib/App/Netdisco/DB/Result/NodeIp.pm index 2d451c63..504c2f7f 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/NodeIp.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/NodeIp.pm @@ -98,6 +98,17 @@ See also the C helper routine, below. __PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node', { 'foreign.mac' => 'self.mac' } ); +=head2 netbios + +Returns the set of C 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' => { diff --git a/Netdisco/lib/App/Netdisco/DB/Result/NodeNbt.pm b/Netdisco/lib/App/Netdisco/DB/Result/NodeNbt.pm index fdde2e15..971ad10a 100644 --- a/Netdisco/lib/App/Netdisco/DB/Result/NodeNbt.pm +++ b/Netdisco/lib/App/Netdisco/DB/Result/NodeNbt.pm @@ -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 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 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 and C tables +include independent C fields. + +See also the C helper routine, below. + +=cut + +__PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node', + { 'foreign.mac' => 'self.mac' } ); + + +=head2 nodeips + +Returns the set of C 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 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 and C tables +include independent C fields. + +=over 4 + +=item * + +Results are ordered by time last seen. + +=item * + +Additional columns C and C provide +preformatted timestamps of the C and C 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 field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +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 field, accurate to the minute. + +The format is somewhat like ISO 8601 or RFC3339 but without the middle C +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 column instantiated into a L 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; diff --git a/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeNbt.pm b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeNbt.pm new file mode 100644 index 00000000..ef2a13b5 --- /dev/null +++ b/Netdisco/lib/App/Netdisco/DB/ResultSet/NodeNbt.pm @@ -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 (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, this returns a ResultSet of matching rows from the +NodeNbt table. + +=over 4 + +=item * + +The C parameter must be a hashref containing a key C with the value +to search for. Value can either be a simple string of IPv4 or IPv6, or a +L 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 and C provide +preformatted timestamps of the C and C fields. + +=item * + +A JOIN is performed on the OUI table and the OUI C column prefetched. + +=back + +To limit results only to active IPs, set C<< {active => 1} >> in C. + +=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, this returns a ResultSet of matching rows from the +NodeNbt table. + +=over 4 + +=item * + +The C parameter must be a hashref containing a key C 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 and C provide +preformatted timestamps of the C and C fields. + +=item * + +A JOIN is performed on the OUI table and the OUI C column prefetched. + +=back + +To limit results only to active IPs, set C<< {active => 1} >> in C. + +=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, this returns a ResultSet of matching rows from the +NodeNbt table. + +=over 4 + +=item * + +The C parameter must be a hashref containing a key C with the value +to search for. + +=item * + +Results are ordered by time last seen. + +=item * + +Additional columns C and C provide +preformatted timestamps of the C and C fields. + +=item * + +A JOIN is performed on the OUI table and the OUI C column prefetched. + +=back + +To limit results only to active IPs, set C<< {active => 1} >> in C. + +=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; diff --git a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm index 28280cb2..fbba07b1 100644 --- a/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm +++ b/Netdisco/lib/App/Netdisco/Web/Plugin/Search/Node.pm @@ -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', { diff --git a/Netdisco/share/views/ajax/search/node_by_ip.tt b/Netdisco/share/views/ajax/search/node_by_ip.tt index c44ef163..556f5dbb 100644 --- a/Netdisco/share/views/ajax/search/node_by_ip.tt +++ b/Netdisco/share/views/ajax/search/node_by_ip.tt @@ -1,4 +1,5 @@ [% USE date(format = '%Y-%m-%d %H:%M') %] +[% USE Number.Format %] @@ -16,6 +17,24 @@ [% WHILE (row = macs.next) %] + [% IF row.nbname %] + + + [% IF params.show_vendor %] + + [% END %] + + + [% IF params.stamps %] + + + [% END %] + + [% ELSE %] [% END %] + [% END %] + [% FOREACH nbt IN row.netbios %] + + + [% IF params.show_vendor %] + + [% END %] + + + [% IF params.stamps %] + + + [% END %] + + [% END %] + [% FOREACH ni IN row.nodeips %] + + + [% IF params.show_vendor %] + + [% END %] + + + [% IF params.stamps %] + + + [% END %] + + [% END %] [% FOREACH node IN row.node_sightings(archive_filter) %] @@ -53,22 +106,6 @@ [% END %] - [% FOREACH nbt IN node.netbios %] - - - [% IF params.show_vendor %] - - [% END %] - - - [% IF params.stamps %] - - - [% END %] - - [% END %] [% FOREACH wlan IN node.wireless %] diff --git a/Netdisco/share/views/ajax/search/node_by_mac.tt b/Netdisco/share/views/ajax/search/node_by_mac.tt index 4cfce4fe..56893d7a 100644 --- a/Netdisco/share/views/ajax/search/node_by_mac.tt +++ b/Netdisco/share/views/ajax/search/node_by_mac.tt @@ -138,8 +138,8 @@
[% nbt.nbuser || '[No User]' | html %]@[% nbt.ip | html_entity %] [% IF params.stamps %] - - + + [% END %] [% SET first_row = 0 %]
+ [% row.net_mac.$mac_format_call | html_entity %][% row.oui.company | html_entity %]NetBIOS\\[% row.domain | html %]\[% row.nbname | html_entity %] +
[% row.nbuser || '[No User]' | html %]@[% row.ip | html_entity %] +
[% row.time_first_stamp | html_entity %][% row.time_last_stamp | html_entity %]
@@ -33,6 +52,40 @@ [% row.time_last_stamp | html_entity %]
  NetBIOS\\[% nbt.domain | html %]\[% nbt.nbname | html_entity %] +
[% nbt.nbuser || '[No User]' | html %]@[% nbt.ip | html_entity %] +
[% date.format(nbt.time_first) | html_entity %][% date.format(nbt.time_last) | html_entity %]
  IP → MAC[% ni.ip | html_entity %] + [% '  ' IF NOT ni.active %] + [% ' (' _ ni.dns.remove(settings.domain_suffix) _ ')' IF ni.dns %] + [% date.format(ni.time_first) | html_entity %][% date.format(ni.time_last) | html_entity %]
 [% node.time_last_stamp | html_entity %]
  NetBIOS\\[% nbt.domain | html %]\[% nbt.nbname | html_entity %] -
[% nbt.nbuser || '[No User]' | html %]@[% nbt.ip | html_entity %] -
[% date.format(nbt.time_first) | html_entity %][% date.format(nbt.time_last) | html_entity %]
  [% nbt.get_column('time_first_stamp') | html_entity %][% nbt.get_column('time_last_stamp') | html_entity %][% nbt.time_first_stamp | html_entity %][% nbt.time_last_stamp | html_entity %]