1package App::Netdisco::Util::Nbtstat;
2
3use Dancer qw/:syntax :script/;
4use Dancer::Plugin::DBIC 'schema';
5
6use App::Netdisco::Util::Node 'check_mac';
7use App::Netdisco::AnyEvent::Nbtstat;
8use Encode;
9
10use base 'Exporter';
11our @EXPORT = ();
12our @EXPORT_OK = qw/ nbtstat_resolve_async store_nbt /;
13our %EXPORT_TAGS = (all => \@EXPORT_OK);
14
15=head1 NAME
16
17App::Netdisco::Util::Nbtstat
18
19=head1 DESCRIPTION
20
21Helper subroutines to support parts of the Netdisco application.
22
23There are no default exports, however the C<:all> tag will export all
24subroutines.
25
26=head1 EXPORT_OK
27
28=head2 nbtstat_resolve_async( $ips )
29
30This method uses an asynchronous AnyEvent NetBIOS node status requester
31C<App::Netdisco::AnyEvent::Nbtstat>.
32
33Given a reference to an array of hashes will connects to the C<IPv4> of a
34node and gets NetBIOS node status information.
35
36Returns the supplied reference to an array of hashes with MAC address,
37NetBIOS name, NetBIOS domain/workgroup, NetBIOS user, and NetBIOS server
38service status for addresses which responded.
39
40=cut
41
42sub nbtstat_resolve_async {
43    my $ips = shift;
44
45    my $timeout  = (setting('nbtstat_response_timeout')
46      || setting('nbtstat_timeout') ||  1);
47    my $interval = setting('nbtstat_interval') || 0.02;
48
49    my $stater = App::Netdisco::AnyEvent::Nbtstat->new(
50        timeout  => $timeout,
51        interval => $interval
52    );
53
54    # Set up the condvar
55    my $cv = AE::cv;
56    $cv->begin( sub { shift->send } );
57
58    foreach my $hash_ref (@$ips) {
59        my $ip = $hash_ref->{'ip'};
60        $cv->begin;
61        $stater->nbtstat(
62            $ip,
63            sub {
64                my $res = shift;
65                _filter_nbname( $ip, $hash_ref, $res );
66                $cv->end;
67            }
68        );
69    }
70
71    # Decrement the cv counter to cancel out the send declaration
72    $cv->end;
73
74    # Wait for the resolver to perform all resolutions
75    $cv->recv;
76
77    # Close sockets
78    undef $stater;
79
80    return $ips;
81}
82
83# filter nbt names / information
84sub _filter_nbname {
85    my $ip          = shift;
86    my $hash_ref = shift;
87    my $node_status = shift;
88
89    my $server = 0;
90    my $nbname = '';
91    my $domain = '';
92    my $nbuser = '';
93
94    for my $rr ( @{$node_status->{'names'}} ) {
95        my $suffix = defined $rr->{'suffix'} ? $rr->{'suffix'} : -1;
96        my $G      = defined $rr->{'G'}      ? $rr->{'G'}      : '';
97        my $name   = defined $rr->{'name'}   ? $rr->{'name'}   : '';
98
99        if ( $suffix == 0 and $G eq "GROUP" ) {
100            $domain = $name;
101        }
102        if ( $suffix == 3 and $G eq "UNIQUE" ) {
103            $nbuser = $name;
104        }
105        if ( $suffix == 0 and $G eq "UNIQUE" ) {
106            $nbname = $name unless $name =~ /^IS~/;
107        }
108        if ( $suffix == 32 and $G eq "UNIQUE" ) {
109            $server = 1;
110        }
111    }
112
113    unless ($nbname) {
114      debug sprintf ' nbtstat no computer name found for %s', $ip;
115        return;
116    }
117
118    my $mac = $node_status->{'mac_address'} || '';
119
120    unless ( check_mac( $mac, $ip ) ) {
121
122        # Just assume it's the last MAC we saw this IP at.
123        my $node_ip = schema('netdisco')->resultset('NodeIp')
124            ->single( { ip => $ip, -bool => 'active' } );
125
126        if ( !defined $node_ip ) {
127            debug sprintf ' no MAC for %s returned by nbtstat or in DB', $ip;
128            return;
129        }
130        $mac = $node_ip->mac;
131    }
132
133    $hash_ref->{'ip'} = $ip;
134    $hash_ref->{'mac'}    = $mac;
135    $hash_ref->{'nbname'} = Encode::decode('UTF-8', $nbname);
136    $hash_ref->{'domain'} = Encode::decode('UTF-8', $domain);
137    $hash_ref->{'server'} = $server;
138    $hash_ref->{'nbuser'} = Encode::decode('UTF-8', $nbuser);
139
140    return;
141}
142
143=head2 store_nbt($nb_hash_ref, $now?)
144
145Stores entries in C<node_nbt> table from the provided hash reference; MAC
146C<mac>, IP C<ip>, Unique NetBIOS Node Name C<nbname>, NetBIOS Domain or
147Workgroup C<domain>, whether the Server Service is running C<server>,
148and the current NetBIOS user C<nbuser>.
149
150Adds new entry or time stamps matching one.
151
152Optionally a literal string can be passed in the second argument for the
153C<time_last> timestamp, otherwise the current timestamp (C<now()>) is used.
154
155=cut
156
157sub store_nbt {
158    my ( $hash_ref, $now ) = @_;
159    $now ||= 'now()';
160
161    schema('netdisco')->resultset('NodeNbt')->update_or_create(
162        {   mac       => $hash_ref->{'mac'},
163            ip        => $hash_ref->{'ip'},
164            nbname    => $hash_ref->{'nbname'},
165            domain    => $hash_ref->{'domain'},
166            server    => $hash_ref->{'server'},
167            nbuser    => $hash_ref->{'nbuser'},
168            active    => \'true',
169            time_last => \$now,
170        },
171        {   key => 'primary',
172            for => 'update',
173        }
174    );
175
176    return;
177}
178
1791;
180