relocate repo files so ND2 is the only code

This commit is contained in:
Oliver Gorwits
2017-04-14 23:08:55 +01:00
parent 9a016ea6ba
commit d23b32500f
469 changed files with 0 additions and 6920 deletions

325
lib/App/Netdisco.pm Normal file
View File

@@ -0,0 +1,325 @@
package App::Netdisco;
use strict;
use warnings;
use 5.010_000;
our $VERSION = '2.034003';
use App::Netdisco::Configuration;
use Module::Find ();
Module::Find::usesub 'App::NetdiscoE::Init';
=head1 NAME
App::Netdisco - An open source web-based network management tool.
=head1 DESCRIPTION
Netdisco is a web-based network management tool designed for network
administrators. Data is collected into a PostgreSQL database using SNMP.
Some of the things you can do with Netdisco:
=over 4
=item *
B<Locate> a machine on the network by MAC or IP and show the switch port it
lives at
=item *
B<Turn off> a switch port, or change the VLAN or PoE status of a port
=item *
B<Inventory> your network hardware by model, vendor, software and operating
system
=item *
B<Pretty pictures> of your network
=back
L<App::Netdisco> provides a web frontend with built-in web server, and a
backend daemon to handle interactive requests such as changing port or device
properties.
=over 4
=item *
See the demo at: L<http://netdisco2-demo.herokuapp.com/>
=back
If you have any trouble getting installed or running, check out the
L<Deployment|App::Netdisco::Manual::Deployment> and
L<Troubleshooting|App::Netdisco::Manual::Troubleshooting> notes, or speak to
someone in the C<#netdisco@freenode> IRC channel, or on the L<community email
list|https://lists.sourceforge.net/lists/listinfo/netdisco-users>. Before
installing or upgrading please always review the latest L<Release
Notes|App::Netdisco::Manual::ReleaseNotes>.
=head1 Dependencies
Netdisco has several Perl library dependencies which will be automatically
installed. However it's I<strongly> recommended that you first install
L<DBD::Pg>, L<SNMP>, and a compiler using your operating system packages.
On Ubuntu/Debian:
root:~# apt-get install libdbd-pg-perl libsnmp-perl build-essential
On Fedora/Red-Hat:
root:~# yum install perl-core perl-DBD-Pg net-snmp-perl net-snmp-devel make automake gcc
On BSD systems please see L<our BSD tips|App::Netdisco::Manual::BSDInstall>.
With those installed, please check that your system's clock is correct.
Create a user on your system called C<netdisco> if one does not already exist.
We'll install Netdisco and its dependencies into this user's home area, which
will take about 250MB including MIB files.
root:~# useradd -m -p x -s /bin/bash netdisco
Netdisco uses the PostgreSQL database server. Install PostgreSQL (at least
version 8.4) and then change to the PostgreSQL superuser (usually
C<postgres>). Create a new database and PostgreSQL user for the Netdisco
application:
root:~# su - postgres
postgres:~$ createuser -DRSP netdisco
Enter password for new role:
Enter it again:
postgres:~$ createdb -O netdisco netdisco
The default PostgreSQL configuration isn't well tuned for modern server
hardware. We strongly recommend that you use the C<pgtune> Python program to
auto-tune your C<postgresql.conf> file:
=over 4
=item *
L<https://github.com/elitwin/pgtune>
=back
=head1 Installation
The following is a general guide which works well in most circumstances. It
assumes you have a user C<netdisco> on your system, that you want to perform
an on-line installation, and have the application run self-contained from
within that user's home. There are alternatives: see the
L<Deployment|App::Netdisco::Manual::Deployment> documentation for further
details.
To avoid muddying your system, use the following script to download and
install Netdisco and its dependencies into the C<netdisco> user's home area
(C<~netdisco/perl5>):
su - netdisco
curl -L http://cpanmin.us/ | perl - --notest --local-lib ~/perl5 App::Netdisco
Link some of the newly installed apps into a handy location:
mkdir ~/bin
ln -s ~/perl5/bin/{localenv,netdisco-*} ~/bin/
Test the installation by running the following command, which should only
produce a status message (it's just a test - you'll start the daemon properly,
later on):
~/bin/netdisco-daemon status
=head1 Configuration
Make a directory for your local configuration and copy the configuration
template from this distribution:
mkdir ~/environments
cp ~/perl5/lib/perl5/auto/share/dist/App-Netdisco/environments/deployment.yml ~/environments
chmod 600 ~/environments/deployment.yml
Edit the file ("C<~/environments/deployment.yml>") and change the database
connection parameters to match those for your local system (that is, the
C<name>, C<user> and C<pass>).
In the same file uncomment and edit the C<domain_suffix> setting to be
appropriate for your local site.
Change the C<community> string setting if your site has different values, and
uncomment the C<schedule> setting to enable SNMP data gathering from
devices (this replaces cron jobs in Netdisco 1).
Have a quick read of the other settings to make sure you're happy, then move
on. See L<Configuration|App::Netdisco::Manual::Configuration> for further
details.
=head1 Bootstrap
The database either needs configuring if new, or updating from the current
release of Netdisco (1.x). You also need vendor MAC address prefixes (OUI
data) and some MIBs if you want to run the daemon. The following script will
take care of all this for you:
~/bin/netdisco-deploy
If this is a new installation of Netdisco 2, answer yes to all questions. If
you wish to deploy without Internet access, see the
L<Deployment|App::Netdisco::Manual::Deployment> documentation.
=head1 Startup
Run the following command to start the web-app server as a backgrounded daemon
(listening on port 5000):
~/bin/netdisco-web start
Run the following command to start the job control daemon (port control, etc):
~/bin/netdisco-daemon start
You should take care not to run this Netdisco daemon and the Netdisco 1.x
daemon at the same time. Similarly, if you use the device discovery with
Netdisco 2, disable your system's cron jobs for the Netdisco 1.x poller.
For further documentation on deployment, see
L<Deployment|App::Netdisco::Manual::Deployment>. If you think Netdisco isn't
behaving correctly, see also the
L<Troubleshooting|App::Netdisco::Manual::Troubleshooting> page.
=head1 Upgrading from 2.x
If you're running a version of Netdisco prior to 2.x then you should follow
the full installation instructions, above. This process is for upgrading
version 2.x only.
Before upgrading please review the latest L<Release
Notes|App::Netdisco::Manual::ReleaseNotes>. Then, the process is as follows:
# upgrade Netdisco
~/bin/localenv cpanm --notest App::Netdisco
# apply database schema updates
~/bin/netdisco-deploy
# restart web service
~/bin/netdisco-web restart
# restart job daemon (if you use it)
~/bin/netdisco-daemon restart
=head1 Tips and Tricks
=head2 Searching
The main black navigation bar has a search box which is smart enough to work
out what you're looking for in most cases. For example device names, node IP
or MAC addreses, VLAN numbers, and so on.
=head2 Command-Line Device and Port Actions
Most significant Device jobs and Port actions, as well as several
troubleshooting and housekeeping duties, can be performed at the command-ling
with the L<netdisco-do> program. For example:
~/bin/netdisco-do -D discover -d 192.0.2.1
See the L<netdisco-do documentation|netdisco-do> for further details.
=head2 Import Topology
Netdisco 1.x had support for a topology information file to fill in device
port relations which could not be discovered. This is now stored in the
database (and edited in the web interface). To import a legacy topology file,
run:
~/bin/localenv nd-import-topology /path/to/netdisco-topology.txt
=head2 Database API
Bundled with this distribution is a L<DBIx::Class> layer for the Netdisco
database. This abstracts away all the SQL into an elegant, re-usable OO
interface. See the L<Developer|App::Netdisco::Manual::Developing>
documentation for further information.
=head2 Plugins
Netdisco includes a Plugin subsystem for customizing the web user interface.
See L<App::Netdisco::Web::Plugin> for further information.
=head2 Developing
Lots of information about the architecture of this application is contained
within the L<Developer|App::Netdisco::Manual::Developing> documentation.
=head1 AUTHOR
Oliver Gorwits <oliver@cpan.org>
=head1 CONTRIBUTORS
Netdisco was created at the University of California, Santa Cruz (UCSC),
Networking and Technology Services (NTS) department. UCSC continues to support
the development of Netdisco by providing development servers and beer.
Original development by Max Baker, with significant contributions from Mark
Boolootian and Jim Warner (through whose ideas Netdisco was born and shaped),
Bill Fenner, Jeroen van Ingen, Eric Miller, Carlos Vicente, and Brian de Wolf.
Other contributions (large and small) by Mike Hunter (UCB), Brian Wilson
(NCSU), Bradley Baetz (bbaetz), David Temkin (sig.com), Edson Manners (FSU),
Dmitry Sergienko (Trifle Co, .ua), Remo Rickli (PSI, Switzerland),
Jean-Philippe Luiggi (sagem.com), A.L.M Buxey (Loughborough University, UK),
Kevin Cheek (UMICH), John Bigrow (bnl.gov), George Pavel (llnl.gov), Charles
Goldsmith (wokka.org), Douglas M. McKeown (saintmarys.edu), Revital Shvarzman
(York U, Ontario), Walter Gould (Auburn U), Lindsay Druet and Colin Palmer (U
of Waikato, Hamilton NZ), Dusty Hall (Auburn U), Jon Monroe (center pointe),
Alexander Barthel, Bill Anderson, Alexander Hartmaier (t-systems.at), Justin
Hunter (Arizona State U), Jethro Binks (U of Strathclyde, Glasgow), Jordi
Guijarro (UAB.es), Sam Stickland (spacething.org), Stefan Radman (CTBTO.org),
Clint Wise, Max Kosmach, and Bernhard Augenstein.
We probably forgot some names - sorry about that :-(.
Deep gratitude also goes
to the authors and communities of all the other software that Netdisco is
built upon.
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2012-2015 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:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the Netdisco Project nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE NETDISCO DEVELOPER TEAM BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
=cut
1;

View File

@@ -0,0 +1,280 @@
package App::Netdisco::AnyEvent::Nbtstat;
use strict;
use warnings;
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

@@ -0,0 +1,96 @@
package App::Netdisco::Configuration;
use App::Netdisco::Environment;
use Dancer ':script';
BEGIN {
# stuff useful locations into @INC
unshift @INC, @{ (setting('include_paths') || []) };
}
# set up database schema config from simple config vars
if (ref {} eq ref setting('database')) {
my $name = ($ENV{NETDISCO_DBNAME} || setting('database')->{name} || 'netdisco');
my $host = setting('database')->{host};
my $user = setting('database')->{user};
my $pass = setting('database')->{pass};
my $dsn = "dbi:Pg:dbname=${name}";
$dsn .= ";host=${host}" if $host;
# set up the netdisco schema now we have access to the config
# but only if it doesn't exist from an earlier config style
setting('plugins')->{DBIC}->{netdisco} ||= {
dsn => $dsn,
user => $user,
password => $pass,
options => {
AutoCommit => 1,
RaiseError => 1,
auto_savepoint => 1,
pg_enable_utf8 => 1,
},
schema_class => 'App::Netdisco::DB',
};
foreach my $c (@{setting('external_databases')}) {
my $schema = delete $c->{tag} or next;
next if $schema eq 'netdisco';
setting('plugins')->{DBIC}->{$schema} = $c;
setting('plugins')->{DBIC}->{$schema}->{schema_class}
||= 'App::Netdisco::GenericDB';
}
}
# defaults for workers
setting('workers')->{queue} ||= 'PostgreSQL';
if (exists setting('workers')->{interactives}
or exists setting('workers')->{pollers}) {
setting('workers')->{tasks} =
(setting('workers')->{pollers} || 0)
+ (setting('workers')->{interactives} || 0);
delete setting('workers')->{pollers};
delete setting('workers')->{interactives};
}
# force skipped DNS resolution, if unset
setting('dns')->{hosts_file} ||= '/etc/hosts';
setting('dns')->{no} ||= ['fe80::/64','169.254.0.0/16'];
# legacy config item names
config->{'devport_vlan_limit'} =
config->{'deviceport_vlan_membership_threshold'}
if setting('deviceport_vlan_membership_threshold')
and not setting('devport_vlan_limit');
delete config->{'deviceport_vlan_membership_threshold'};
config->{'schedule'} = config->{'housekeeping'}
if setting('housekeeping') and not setting('schedule');
delete config->{'housekeeping'};
# schedule expire used to be called expiry
setting('schedule')->{expire} ||= setting('schedule')->{expiry}
if setting('schedule') and exists setting('schedule')->{expiry};
delete config->{'schedule'}->{'expiry'} if setting('schedule');
# upgrade reports config from hash to list
if (setting('reports') and ref {} eq ref setting('reports')) {
config->{'reports'} = [ map {{
tag => $_,
%{ setting('reports')->{$_} }
}} keys %{ setting('reports') } ];
}
# set max outstanding requests for AnyEvent::DNS
$ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'}
= setting('dns')->{max_outstanding} || 50;
$ENV{'PERL_ANYEVENT_HOSTS'}
= setting('dns')->{hosts_file} || '/etc/hosts';
# always set this
$ENV{DBIC_TRACE_PROFILE} = 'console';
true;

View File

@@ -0,0 +1,192 @@
package App::Netdisco::Core::Arpnip;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Node 'check_mac';
use App::Netdisco::Util::DNS ':all';
use NetAddr::IP::Lite ':lower';
use Time::HiRes 'gettimeofday';
use NetAddr::MAC ();
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/ do_arpnip store_arp /;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME
App::Netdisco::Core::Arpnip
=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_arpnip( $device, $snmp )
Given a Device database object, and a working SNMP connection, connect to a
device and discover its ARP cache for IPv4 and Neighbor cache for IPv6.
Will also discover subnets in use on the device and update the Subnets table.
=cut
sub do_arpnip {
my ($device, $snmp) = @_;
unless ($device->in_storage) {
debug sprintf ' [%s] arpnip - skipping device not yet discovered', $device->ip;
return;
}
# get v4 arp table
my $v4 = _get_arps($device, $snmp->at_paddr, $snmp->at_netaddr);
# get v6 neighbor cache
my $v6 = _get_arps($device, $snmp->ipv6_n2p_mac, $snmp->ipv6_n2p_addr);
# get directly connected networks
my @subnets = _gather_subnets($device, $snmp);
# TODO: IPv6 subnets
# would be possible just to use now() on updated records, but by using this
# same value for them all, we _can_ if we want add a job at the end to
# select and do something with the updated set (no reason to yet, though)
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
# update node_ip with ARP and Neighbor Cache entries
store_arp(\%$_, $now) for @$v4;
debug sprintf ' [%s] arpnip - processed %s ARP Cache entries',
$device->ip, scalar @$v4;
store_arp(\%$_, $now) for @$v6;
debug sprintf ' [%s] arpnip - processed %s IPv6 Neighbor Cache entries',
$device->ip, scalar @$v6;
_store_subnet($_, $now) for @subnets;
debug sprintf ' [%s] arpnip - processed %s Subnet entries',
$device->ip, scalar @subnets;
$device->update({last_arpnip => \$now});
}
# get an arp table (v4 or v6)
sub _get_arps {
my ($device, $paddr, $netaddr) = @_;
my @arps = ();
while (my ($arp, $node) = each %$paddr) {
my $ip = $netaddr->{$arp};
next unless defined $ip;
next unless check_mac($device, $node);
push @arps, {
node => $node,
ip => $ip,
dns => undef,
};
}
debug sprintf ' resolving %d ARP entries with max %d outstanding requests',
scalar @arps, $ENV{'PERL_ANYEVENT_MAX_OUTSTANDING_DNS'};
my $resolved_ips = hostnames_resolve_async(\@arps);
return $resolved_ips;
}
=head2 store_arp( \%host, $now? )
Stores a new entry to the C<node_ip> table with the given MAC, IP (v4 or v6)
and DNS host name. Host details are provided in a Hash ref:
{
ip => '192.0.2.1',
node => '00:11:22:33:44:55',
dns => 'myhost.example.com',
}
The C<dns> entry is optional. The update will mark old entries for this IP as
no longer C<active>.
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_arp {
my ($hash_ref, $now) = @_;
$now ||= 'now()';
my $ip = $hash_ref->{'ip'};
my $mac = NetAddr::MAC->new($hash_ref->{'node'});
my $name = $hash_ref->{'dns'};
return if !defined $mac or $mac->errstr;
schema('netdisco')->txn_do(sub {
my $current = schema('netdisco')->resultset('NodeIp')
->search(
{ ip => $ip, -bool => 'active'},
{ columns => [qw/mac ip/] })->update({active => \'false'});
schema('netdisco')->resultset('NodeIp')
->update_or_create(
{
mac => $mac->as_ieee,
ip => $ip,
dns => $name,
active => \'true',
time_last => \$now,
},
{
key => 'primary',
for => 'update',
});
});
}
# gathers device subnets
sub _gather_subnets {
my ($device, $snmp) = @_;
my @subnets = ();
my $ip_netmask = $snmp->ip_netmask;
my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8');
foreach my $entry (keys %$ip_netmask) {
my $ip = NetAddr::IP::Lite->new($entry);
my $addr = $ip->addr;
next if $addr eq '0.0.0.0';
next if $ip->within($localnet);
next if setting('ignore_private_nets') and $ip->is_rfc1918;
my $netmask = $ip_netmask->{$addr};
next if $netmask eq '255.255.255.255' or $netmask eq '0.0.0.0';
my $cidr = NetAddr::IP::Lite->new($addr, $netmask)->network->cidr;
debug sprintf ' [%s] arpnip - found subnet %s', $device->ip, $cidr;
push @subnets, $cidr;
}
return @subnets;
}
# update subnets with new networks
sub _store_subnet {
my ($subnet, $now) = @_;
schema('netdisco')->txn_do(sub {
schema('netdisco')->resultset('Subnet')->update_or_create(
{
net => $subnet,
last_discover => \$now,
},
{ for => 'update' });
});
}
1;

View File

@@ -0,0 +1,985 @@
package App::Netdisco::Core::Discover;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Device
qw/get_device match_devicetype is_discoverable/;
use App::Netdisco::Util::DNS ':all';
use App::Netdisco::JobQueue qw/jq_queued jq_insert/;
use NetAddr::IP::Lite ':lower';
use List::MoreUtils ();
use Scalar::Util 'blessed';
use Encode;
use Try::Tiny;
use NetAddr::MAC;
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/
set_canonical_ip
store_device store_interfaces store_wireless
store_vlans store_power store_modules
store_neighbors discover_new_neighbors
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME
App::Netdisco::Core::Discover
=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 set_canonical_ip( $device, $snmp )
Returns: C<$device>
Given a Device database object, and a working SNMP connection, check whether
the database object's IP is the best choice for that device. If not, update
the IP and hostname in the device object for the canonical IP.
=cut
sub set_canonical_ip {
my ($device, $snmp) = @_;
my $old_ip = $device->ip;
my $new_ip = $old_ip;
my $revname = ipv4_from_hostname($snmp->name);
if (setting('reverse_sysname') and $revname) {
$new_ip = $revname;
}
if (setting('device_identity')) {
}
return if $new_ip eq $old_ip;
if (not $snmp->snmp_connect_ip( $new_ip )) {
# should be warning or error?
debug sprintf ' [%s] device - cannot change IP to %s - SNMP connect failed',
$old_ip, $device->ip;
return;
}
schema('netdisco')->txn_do(sub {
$device->renumber($new_ip)
or die "cannot renumber to: $new_ip"; # rollback
debug sprintf ' [%s] device - changed IP to %s (%s)',
$old_ip, $device->ip, ($device->dns || '');
});
}
=head2 store_device( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store basic device information.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
=cut
sub store_device {
my ($device, $snmp) = @_;
my $ip_index = $snmp->ip_index;
my $interfaces = $snmp->interfaces;
my $ip_netmask = $snmp->ip_netmask;
my $localnet = NetAddr::IP::Lite->new('127.0.0.0/8');
# build device aliases suitable for DBIC
my @aliases;
foreach my $entry (keys %$ip_index) {
my $ip = NetAddr::IP::Lite->new($entry)
or next;
my $addr = $ip->addr;
next if $addr eq '0.0.0.0';
next if $ip->within($localnet);
next if setting('ignore_private_nets') and $ip->is_rfc1918;
my $iid = $ip_index->{$addr};
my $port = $interfaces->{$iid};
my $subnet = $ip_netmask->{$addr}
? NetAddr::IP::Lite->new($addr, $ip_netmask->{$addr})->network->cidr
: undef;
debug sprintf ' [%s] device - aliased as %s', $device->ip, $addr;
push @aliases, {
alias => $addr,
port => $port,
subnet => $subnet,
dns => undef,
};
}
debug sprintf ' resolving %d aliases with max %d outstanding requests',
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 => $device->dns }
if 0 == scalar @aliases;
# VTP Management Domain -- assume only one.
my $vtpdomains = $snmp->vtp_d_name;
my $vtpdomain;
if (defined $vtpdomains and scalar values %$vtpdomains) {
$device->set_column( vtp_domain => (values %$vtpdomains)[-1] );
}
my $hostname = hostname_from_ip($device->ip);
$device->set_column( dns => $hostname ) if $hostname;
my @properties = qw/
snmp_ver
description uptime contact name location
layers ports mac
ps1_type ps2_type ps1_status ps2_status
fan slots
vendor os os_ver
/;
foreach my $property (@properties) {
$device->set_column( $property => $snmp->$property );
}
$device->set_column( model => Encode::decode('UTF-8', $snmp->model) );
$device->set_column( serial => Encode::decode('UTF-8', $snmp->serial) );
$device->set_column( snmp_class => $snmp->class );
$device->set_column( last_discover => \'now()' );
schema('netdisco')->txn_do(sub {
my $gone = $device->device_ips->delete;
debug sprintf ' [%s] device - removed %d aliases',
$device->ip, $gone;
$device->update_or_insert(undef, {for => 'update'});
$device->device_ips->populate($resolved_aliases);
debug sprintf ' [%s] device - added %d new aliases',
$device->ip, scalar @aliases;
});
}
=head2 store_interfaces( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's interface/port information.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
=cut
sub store_interfaces {
my ($device, $snmp) = @_;
my $interfaces = $snmp->interfaces;
my $i_type = $snmp->i_type;
my $i_ignore = $snmp->i_ignore;
my $i_descr = $snmp->i_description;
my $i_mtu = $snmp->i_mtu;
my $i_speed = $snmp->i_speed;
my $i_mac = $snmp->i_mac;
my $i_up = $snmp->i_up;
my $i_up_admin = $snmp->i_up_admin;
my $i_name = $snmp->i_name;
my $i_duplex = $snmp->i_duplex;
my $i_duplex_admin = $snmp->i_duplex_admin;
my $i_stp_state = $snmp->i_stp_state;
my $i_vlan = $snmp->i_vlan;
my $i_lastchange = $snmp->i_lastchange;
my $agg_ports = $snmp->agg_ports;
# clear the cached uptime and get a new one
my $dev_uptime = $snmp->load_uptime;
if (!defined $dev_uptime) {
error sprintf ' [%s] interfaces - Error! Failed to get uptime from device!',
$device->ip;
return;
}
# used to track how many times the device uptime wrapped
my $dev_uptime_wrapped = 0;
# use SNMP-FRAMEWORK-MIB::snmpEngineTime if available to
# fix device uptime if wrapped
if (defined $snmp->snmpEngineTime) {
$dev_uptime_wrapped = int( $snmp->snmpEngineTime * 100 / 2**32 );
if ($dev_uptime_wrapped > 0) {
info sprintf ' [%s] interface - device uptime wrapped %d times - correcting',
$device->ip, $dev_uptime_wrapped;
$device->uptime( $dev_uptime + $dev_uptime_wrapped * 2**32 );
}
}
# build device interfaces suitable for DBIC
my %interfaces;
foreach my $entry (keys %$interfaces) {
my $port = $interfaces->{$entry};
if (not $port) {
debug sprintf ' [%s] interfaces - ignoring %s (no port mapping)',
$device->ip, $entry;
next;
}
if (scalar grep {$port =~ m/^$_$/} @{setting('ignore_interfaces') || []}) {
debug sprintf
' [%s] interfaces - ignoring %s (%s) (config:ignore_interfaces)',
$device->ip, $entry, $port;
next;
}
if (exists $i_ignore->{$entry}) {
debug sprintf ' [%s] interfaces - ignoring %s (%s) (%s)',
$device->ip, $entry, $port, $i_type->{$entry};
next;
}
my $lc = $i_lastchange->{$entry} || 0;
if (not $dev_uptime_wrapped and $lc > $dev_uptime) {
info sprintf ' [%s] interfaces - device uptime wrapped (%s) - correcting',
$device->ip, $port;
$device->uptime( $dev_uptime + 2**32 );
$dev_uptime_wrapped = 1;
}
if ($device->is_column_changed('uptime') and $lc) {
if ($lc < $dev_uptime) {
# ambiguous: lastchange could be sysUptime before or after wrap
if ($dev_uptime > 30000 and $lc < 30000) {
# uptime wrap more than 5min ago but lastchange within 5min
# assume lastchange was directly after boot -> no action
}
else {
# uptime wrap less than 5min ago or lastchange > 5min ago
# to be on safe side, assume lastchange after counter wrap
debug sprintf
' [%s] interfaces - correcting LastChange for %s, assuming sysUptime wrap',
$device->ip, $port;
$lc += $dev_uptime_wrapped * 2**32;
}
}
}
$interfaces{$port} = {
port => $port,
descr => $i_descr->{$entry},
up => $i_up->{$entry},
up_admin => $i_up_admin->{$entry},
mac => $i_mac->{$entry},
speed => $i_speed->{$entry},
mtu => $i_mtu->{$entry},
name => Encode::decode('UTF-8', $i_name->{$entry}),
duplex => $i_duplex->{$entry},
duplex_admin => $i_duplex_admin->{$entry},
stp => $i_stp_state->{$entry},
type => $i_type->{$entry},
vlan => $i_vlan->{$entry},
pvid => $i_vlan->{$entry},
is_master => 'false',
slave_of => undef,
lastchange => $lc,
};
}
# must do this after building %interfaces so that we can set is_master
foreach my $sidx (keys %$agg_ports) {
my $slave = $interfaces->{$sidx} or next;
my $master = $interfaces->{ $agg_ports->{$sidx} } or next;
next unless exists $interfaces{$slave} and exists $interfaces{$master};
$interfaces{$slave}->{slave_of} = $master;
$interfaces{$master}->{is_master} = 'true';
}
schema('netdisco')->resultset('DevicePort')->txn_do_locked(sub {
my $gone = $device->ports->delete({keep_nodes => 1});
debug sprintf ' [%s] interfaces - removed %d interfaces',
$device->ip, $gone;
$device->update_or_insert(undef, {for => 'update'});
$device->ports->populate([values %interfaces]);
debug sprintf ' [%s] interfaces - added %d new interfaces',
$device->ip, scalar values %interfaces;
});
}
=head2 store_wireless( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's wireless interface information.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
=cut
sub store_wireless {
my ($device, $snmp) = @_;
my $ssidlist = $snmp->i_ssidlist;
return unless scalar keys %$ssidlist;
my $interfaces = $snmp->interfaces;
my $ssidbcast = $snmp->i_ssidbcast;
my $ssidmac = $snmp->i_ssidmac;
my $channel = $snmp->i_80211channel;
my $power = $snmp->dot11_cur_tx_pwr_mw;
# build device ssid list suitable for DBIC
my @ssids;
foreach my $entry (keys %$ssidlist) {
(my $iid = $entry) =~ s/\.\d+$//;
my $port = $interfaces->{$iid};
if (not $port) {
debug sprintf ' [%s] wireless - ignoring %s (no port mapping)',
$device->ip, $iid;
next;
}
push @ssids, {
port => $port,
ssid => $ssidlist->{$entry},
broadcast => $ssidbcast->{$entry},
bssid => $ssidmac->{$entry},
};
}
schema('netdisco')->txn_do(sub {
my $gone = $device->ssids->delete;
debug sprintf ' [%s] wireless - removed %d SSIDs',
$device->ip, $gone;
$device->ssids->populate(\@ssids);
debug sprintf ' [%s] wireless - added %d new SSIDs',
$device->ip, scalar @ssids;
});
# build device channel list suitable for DBIC
my @channels;
foreach my $entry (keys %$channel) {
my $port = $interfaces->{$entry};
if (not $port) {
debug sprintf ' [%s] wireless - ignoring %s (no port mapping)',
$device->ip, $entry;
next;
}
push @channels, {
port => $port,
channel => $channel->{$entry},
power => $power->{$entry},
};
}
schema('netdisco')->txn_do(sub {
my $gone = $device->wireless_ports->delete;
debug sprintf ' [%s] wireless - removed %d wireless channels',
$device->ip, $gone;
$device->wireless_ports->populate(\@channels);
debug sprintf ' [%s] wireless - added %d new wireless channels',
$device->ip, scalar @channels;
});
}
=head2 store_vlans( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's vlan information.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
=cut
sub store_vlans {
my ($device, $snmp) = @_;
my $v_name = $snmp->v_name;
my $v_index = $snmp->v_index;
# build device vlans suitable for DBIC
my %v_seen = ();
my @devicevlans;
foreach my $entry (keys %$v_name) {
my $vlan = $v_index->{$entry};
next unless defined $vlan and $vlan;
++$v_seen{$vlan};
push @devicevlans, {
vlan => $vlan,
description => $v_name->{$entry},
last_discover => \'now()',
};
}
my $i_vlan = $snmp->i_vlan;
my $i_vlan_membership = $snmp->i_vlan_membership;
my $i_vlan_type = $snmp->i_vlan_type;
my $interfaces = $snmp->interfaces;
# build device port vlans suitable for DBIC
my @portvlans = ();
foreach my $entry (keys %$i_vlan_membership) {
my %port_vseen = ();
my $port = $interfaces->{$entry};
next unless defined $port;
my $type = $i_vlan_type->{$entry};
foreach my $vlan (@{ $i_vlan_membership->{$entry} }) {
next unless defined $vlan and $vlan;
next if ++$port_vseen{$vlan} > 1;
my $native = ((defined $i_vlan->{$entry}) and ($vlan eq $i_vlan->{$entry})) ? "t" : "f";
push @portvlans, {
port => $port,
vlan => $vlan,
native => $native,
vlantype => $type,
last_discover => \'now()',
};
next if $v_seen{$vlan};
# also add an unnamed vlan to the device
push @devicevlans, {
vlan => $vlan,
description => (sprintf "VLAN %d", $vlan),
last_discover => \'now()',
};
++$v_seen{$vlan};
}
}
schema('netdisco')->txn_do(sub {
my $gone = $device->vlans->delete;
debug sprintf ' [%s] vlans - removed %d device VLANs',
$device->ip, $gone;
$device->vlans->populate(\@devicevlans);
debug sprintf ' [%s] vlans - added %d new device VLANs',
$device->ip, scalar @devicevlans;
});
schema('netdisco')->txn_do(sub {
my $gone = $device->port_vlans->delete;
debug sprintf ' [%s] vlans - removed %d port VLANs',
$device->ip, $gone;
$device->port_vlans->populate(\@portvlans);
debug sprintf ' [%s] vlans - added %d new port VLANs',
$device->ip, scalar @portvlans;
});
}
=head2 store_power( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's PoE information.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
=cut
sub store_power {
my ($device, $snmp) = @_;
my $p_watts = $snmp->peth_power_watts;
my $p_status = $snmp->peth_power_status;
if (!defined $p_watts) {
debug sprintf ' [%s] power - 0 power modules', $device->ip;
return;
}
# build device module power info suitable for DBIC
my @devicepower;
foreach my $entry (keys %$p_watts) {
push @devicepower, {
module => $entry,
power => $p_watts->{$entry},
status => $p_status->{$entry},
};
}
my $interfaces = $snmp->interfaces;
my $p_ifindex = $snmp->peth_port_ifindex;
my $p_admin = $snmp->peth_port_admin;
my $p_pstatus = $snmp->peth_port_status;
my $p_class = $snmp->peth_port_class;
my $p_power = $snmp->peth_port_power;
# build device port power info suitable for DBIC
my @portpower;
foreach my $entry (keys %$p_ifindex) {
my $port = $interfaces->{ $p_ifindex->{$entry} };
next unless $port;
my ($module) = split m/\./, $entry;
push @portpower, {
port => $port,
module => $module,
admin => $p_admin->{$entry},
status => $p_pstatus->{$entry},
class => $p_class->{$entry},
power => $p_power->{$entry},
};
}
schema('netdisco')->txn_do(sub {
my $gone = $device->power_modules->delete;
debug sprintf ' [%s] power - removed %d power modules',
$device->ip, $gone;
$device->power_modules->populate(\@devicepower);
debug sprintf ' [%s] power - added %d new power modules',
$device->ip, scalar @devicepower;
});
schema('netdisco')->txn_do(sub {
my $gone = $device->powered_ports->delete;
debug sprintf ' [%s] power - removed %d PoE capable ports',
$device->ip, $gone;
$device->powered_ports->populate(\@portpower);
debug sprintf ' [%s] power - added %d new PoE capable ports',
$device->ip, scalar @portpower;
});
}
=head2 store_modules( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's module information.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
=cut
sub store_modules {
my ($device, $snmp) = @_;
my $e_index = $snmp->e_index;
if (!defined $e_index) {
schema('netdisco')->txn_do(sub {
my $gone = $device->modules->delete;
debug sprintf ' [%s] modules - removed %d chassis modules',
$device->ip, $gone;
$device->modules->update_or_create({
ip => $device->ip,
index => 1,
parent => 0,
name => 'chassis',
class => 'chassis',
pos => -1,
# too verbose and link doesn't work anyway
# description => $device->description,
sw_ver => $device->os_ver,
serial => $device->serial,
model => $device->model,
fru => \'false',
last_discover => \'now()',
});
});
debug
sprintf ' [%s] modules - 0 chassis components (added one pseudo for chassis)',
$device->ip;
return;
}
my $e_descr = $snmp->e_descr;
my $e_type = $snmp->e_type;
my $e_parent = $snmp->e_parent;
my $e_name = $snmp->e_name;
my $e_class = $snmp->e_class;
my $e_pos = $snmp->e_pos;
my $e_hwver = $snmp->e_hwver;
my $e_fwver = $snmp->e_fwver;
my $e_swver = $snmp->e_swver;
my $e_model = $snmp->e_model;
my $e_serial = $snmp->e_serial;
my $e_fru = $snmp->e_fru;
# build device modules list for DBIC
my @modules;
foreach my $entry (keys %$e_index) {
push @modules, {
index => $e_index->{$entry},
type => $e_type->{$entry},
parent => $e_parent->{$entry},
name => Encode::decode('UTF-8', $e_name->{$entry}),
class => $e_class->{$entry},
pos => $e_pos->{$entry},
hw_ver => Encode::decode('UTF-8', $e_hwver->{$entry}),
fw_ver => Encode::decode('UTF-8', $e_fwver->{$entry}),
sw_ver => Encode::decode('UTF-8', $e_swver->{$entry}),
model => Encode::decode('UTF-8', $e_model->{$entry}),
serial => Encode::decode('UTF-8', $e_serial->{$entry}),
fru => $e_fru->{$entry},
description => Encode::decode('UTF-8', $e_descr->{$entry}),
last_discover => \'now()',
};
}
schema('netdisco')->txn_do(sub {
my $gone = $device->modules->delete;
debug sprintf ' [%s] modules - removed %d chassis modules',
$device->ip, $gone;
$device->modules->populate(\@modules);
debug sprintf ' [%s] modules - added %d new chassis modules',
$device->ip, scalar @modules;
});
}
=head2 store_neighbors( $device, $snmp )
returns: C<@to_discover>
Given a Device database object, and a working SNMP connection, discover and
store the device's port neighbors information.
Entries in the Topology database table will override any discovered device
port relationships.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
A list of discovererd neighbors will be returned as [C<$ip>, C<$type>] tuples.
=cut
sub store_neighbors {
my ($device, $snmp) = @_;
my @to_discover = ();
# first allow any manually configured topology to be set
_set_manual_topology($device, $snmp);
my $c_ip = $snmp->c_ip;
unless ($snmp->hasCDP or scalar keys %$c_ip) {
debug sprintf ' [%s] neigh - CDP/LLDP not enabled!', $device->ip;
return @to_discover;
}
my $interfaces = $snmp->interfaces;
my $c_if = $snmp->c_if;
my $c_port = $snmp->c_port;
my $c_id = $snmp->c_id;
my $c_platform = $snmp->c_platform;
my $c_cap = $snmp->c_cap;
foreach my $entry (sort (List::MoreUtils::uniq( (keys %$c_ip), (keys %$c_cap) ))) {
if (!defined $c_if->{$entry} or !defined $interfaces->{ $c_if->{$entry} }) {
debug sprintf ' [%s] neigh - port for IID:%s not resolved, skipping',
$device->ip, $entry;
next;
}
my $port = $interfaces->{ $c_if->{$entry} };
my $portrow = schema('netdisco')->resultset('DevicePort')
->single({ip => $device->ip, port => $port});
if (!defined $portrow) {
info sprintf ' [%s] neigh - local port %s not in database!',
$device->ip, $port;
next;
}
my $remote_ip = $c_ip->{$entry};
my $remote_ipad = NetAddr::IP::Lite->new($remote_ip);
my $remote_port = undef;
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} || [];
# IP Phone and WAP detection type fixup
if (scalar @$remote_cap or $remote_type) {
my $phone_flag = grep {match_devicetype($_, 'phone_capabilities')}
@$remote_cap;
my $ap_flag = grep {match_devicetype($_, 'wap_capabilities')}
@$remote_cap;
if ($phone_flag or match_devicetype($remote_type, 'phone_platforms')) {
$remote_type = 'IP Phone: '. $remote_type
if $remote_type !~ /ip.phone/i;
}
elsif ($ap_flag or match_devicetype($remote_type, 'wap_platforms')) {
$remote_type = 'AP: '. $remote_type;
}
$portrow->update({remote_type => $remote_type});
}
if ($portrow->manual_topo) {
info sprintf ' [%s] neigh - %s has manually defined topology',
$device->ip, $port;
next;
}
next unless $remote_ip;
# a bunch of heuristics to search known devices if we don't have a
# useable remote IP...
if ($remote_ip eq '0.0.0.0' or
$remote_ipad->within(NetAddr::IP::Lite->new('127.0.0.0/8'))) {
if ($remote_id) {
my $devices = schema('netdisco')->resultset('Device');
my $neigh = $devices->single({name => $remote_id});
info sprintf
' [%s] neigh - bad address %s on port %s, searching for %s instead',
$device->ip, $remote_ip, $port, $remote_id;
if (!defined $neigh) {
my $mac = NetAddr::MAC->new(mac => $remote_id);
if ($mac and not $mac->errstr) {
$neigh = $devices->single({mac => $mac->as_ieee});
}
}
# some HP switches send 127.0.0.1 as remote_ip if no ip address
# on default vlan for HP switches remote_ip looks like
# "myswitchname(012345-012345)"
if (!defined $neigh) {
(my $tmpid = $remote_id) =~ s/.*\(([0-9a-f]{6})-([0-9a-f]{6})\).*/$1$2/;
my $mac = NetAddr::MAC->new(mac => $tmpid);
if ($mac and not $mac->errstr) {
info sprintf
'[%s] neigh - found neighbor %s by MAC %s',
$device->ip, $remote_id, $mac->as_ieee;
$neigh = $devices->single({mac => $mac->as_ieee});
}
}
if (!defined $neigh) {
(my $shortid = $remote_id) =~ s/\..*//;
$neigh = $devices->single({name => { -ilike => "${shortid}%" }});
}
if ($neigh) {
$remote_ip = $neigh->ip;
info sprintf ' [%s] neigh - found %s with IP %s',
$device->ip, $remote_id, $remote_ip;
}
else {
info sprintf ' [%s] neigh - could not find %s, skipping',
$device->ip, $remote_id;
next;
}
}
else {
info sprintf ' [%s] neigh - skipping unuseable address %s on port %s',
$device->ip, $remote_ip, $port;
next;
}
}
# hack for devices seeing multiple neighbors on the port
if (ref [] eq ref $remote_ip) {
debug sprintf
' [%s] neigh - port %s has multiple neighbors, setting remote as self',
$device->ip, $port;
if (wantarray) {
foreach my $n (@$remote_ip) {
debug sprintf
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
$device->ip, $n, ($remote_type || ''), $port;
push @to_discover, [$n, $remote_type];
}
}
# set self as remote IP to suppress any further work
$remote_ip = $device->ip;
$remote_port = $port;
}
else {
# what we came here to do.... discover the neighbor
if (wantarray) {
debug sprintf
' [%s] neigh - adding neighbor %s, type [%s], on %s to discovery queue',
$device->ip, $remote_ip, ($remote_type || ''), $port;
push @to_discover, [$remote_ip, $remote_type];
}
# further device type discovery using MAC OUI
# only works once device is fully discovered (so we have a MAC addr)
my $neigh = get_device($remote_ip);
if (blessed $neigh and $neigh->in_storage and $neigh->mac) {
if (match_devicetype($neigh->mac, 'phone_ouis')) {
$remote_type = 'IP Phone: '. $remote_type
if $remote_type !~ /ip.phone/i;
}
elsif (match_devicetype($neigh->mac, 'wap_ouis')) {
$remote_type = 'AP: '. $remote_type
if $remote_type !~ /^AP: /;
}
}
$remote_port = $c_port->{$entry};
if (defined $remote_port) {
# clean weird characters
$remote_port =~ s/[^\d\/\.,()\w:-]+//gi;
}
else {
info sprintf ' [%s] neigh - no remote port found for port %s at %s',
$device->ip, $port, $remote_ip;
}
}
$portrow->update({
remote_ip => $remote_ip,
remote_port => $remote_port,
remote_type => $remote_type,
remote_id => $remote_id,
is_uplink => \"true",
manual_topo => \"false",
});
# update master of our aggregate to be a neighbor of
# the master on our peer device (a lot of iffs to get there...).
# & cannot use ->neighbor prefetch because this is the port insert!
if (defined $portrow->slave_of) {
my $peer_device = get_device($remote_ip);
my $master = schema('netdisco')->resultset('DevicePort')->single({
ip => $device->ip,
port => $portrow->slave_of
});
if ($peer_device and $peer_device->in_storage and $master
and not ($portrow->is_master or defined $master->slave_of)) {
my $peer_port = schema('netdisco')->resultset('DevicePort')->single({
ip => $peer_device->ip,
port => $portrow->remote_port,
});
$master->update({
remote_ip => ($peer_device->ip || $remote_ip),
remote_port => ($peer_port ? $peer_port->slave_of : undef ),
is_uplink => \"true",
is_master => \"true",
manual_topo => \"false",
});
}
}
}
return @to_discover;
}
# take data from the topology table and update remote_ip and remote_port
# in the devices table. only use root_ips and skip any bad topo entries.
sub _set_manual_topology {
my ($device, $snmp) = @_;
schema('netdisco')->txn_do(sub {
# clear manual topology flags
schema('netdisco')->resultset('DevicePort')
->search({ip => $device->ip})->update({manual_topo => \'false'});
my $topo_links = schema('netdisco')->resultset('Topology')
->search({-or => [dev1 => $device->ip, dev2 => $device->ip]});
debug sprintf ' [%s] neigh - setting manual topology links', $device->ip;
while (my $link = $topo_links->next) {
# could fail for broken topo, but we ignore to try the rest
try {
schema('netdisco')->txn_do(sub {
# only work on root_ips
my $left = get_device($link->dev1);
my $right = get_device($link->dev2);
# skip bad entries
return unless ($left->in_storage and $right->in_storage);
$left->ports
->single({port => $link->port1})
->update({
remote_ip => $right->ip,
remote_port => $link->port2,
remote_type => undef,
remote_id => undef,
is_uplink => \"true",
manual_topo => \"true",
});
$right->ports
->single({port => $link->port2})
->update({
remote_ip => $left->ip,
remote_port => $link->port1,
remote_type => undef,
remote_id => undef,
is_uplink => \"true",
manual_topo => \"true",
});
});
};
}
});
}
=head2 discover_new_neighbors( $device, $snmp )
Given a Device database object, and a working SNMP connection, discover and
store the device's port neighbors information.
Entries in the Topology database table will override any discovered device
port relationships.
The Device database object can be a fresh L<DBIx::Class::Row> object which is
not yet stored to the database.
Any discovered neighbor unknown to Netdisco will have a C<discover> job
immediately queued (subject to the filtering by the C<discover_*> settings).
=cut
sub discover_new_neighbors {
my @to_discover = store_neighbors(@_);
# only enqueue if device is not already discovered,
# discover_* config permits the discovery
foreach my $neighbor (@to_discover) {
my ($ip, $remote_type) = @$neighbor;
my $device = get_device($ip);
next if $device->in_storage;
if (not is_discoverable($device, $remote_type)) {
debug sprintf
' queue - %s, type [%s] excluded by discover_* config',
$ip, ($remote_type || '');
next;
}
jq_insert({
device => $ip,
action => 'discover',
subaction => 'with-nodes',
});
}
}
1;

View File

@@ -0,0 +1,543 @@
package App::Netdisco::Core::Macsuck;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::PortMAC 'get_port_macs';
use App::Netdisco::Util::Device qw/check_device_no match_devicetype/;
use App::Netdisco::Util::Node 'check_mac';
use App::Netdisco::Util::SNMP 'snmp_comm_reindex';
use Time::HiRes 'gettimeofday';
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/
do_macsuck
store_node
store_wireless_client_info
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);
=head1 NAME
App::Netdisco::Core::Macsuck
=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_macsuck( $device, $snmp )
Given a Device database object, and a working SNMP connection, connect to a
device and discover the MAC addresses listed against each physical port
without a neighbor.
If the device has VLANs, C<do_macsuck> will walk each VLAN to get the MAC
addresses from there.
It will also gather wireless client information if C<store_wireless_clients>
configuration setting is enabled.
=cut
sub do_macsuck {
my ($device, $snmp) = @_;
unless ($device->in_storage) {
debug sprintf
' [%s] macsuck - skipping device not yet discovered',
$device->ip;
return;
}
my $ip = $device->ip;
# would be possible just to use now() on updated records, but by using this
# same value for them all, we can if we want add a job at the end to
# select and do something with the updated set (see set archive, below)
my $now = 'to_timestamp('. (join '.', gettimeofday) .')';
my $total_nodes = 0;
# do this before we start messing with the snmp community string
store_wireless_client_info($device, $snmp, $now);
# cache the device ports to save hitting the database for many single rows
my $device_ports = {map {($_->port => $_)}
$device->ports(undef, {prefetch => {neighbor_alias => 'device'}})->all};
my $port_macs = get_port_macs();
my $interfaces = $snmp->interfaces;
# get forwarding table data via basic snmp connection
my $fwtable = _walk_fwtable($device, $snmp, $interfaces, $port_macs, $device_ports);
# ...then per-vlan if supported
my @vlan_list = _get_vlan_list($device, $snmp);
foreach my $vlan (@vlan_list) {
snmp_comm_reindex($snmp, $device, $vlan);
my $pv_fwtable = _walk_fwtable($device, $snmp, $interfaces, $port_macs, $device_ports, $vlan);
$fwtable = {%$fwtable, %$pv_fwtable};
}
# now it's time to call store_node for every node discovered
# on every port on every vlan on this device.
# reverse sort allows vlan 0 entries to be included only as fallback
foreach my $vlan (reverse sort keys %$fwtable) {
foreach my $port (keys %{ $fwtable->{$vlan} }) {
debug sprintf ' [%s] macsuck - port %s vlan %s : %s nodes',
$ip, $port, $vlan, scalar keys %{ $fwtable->{$vlan}->{$port} };
# make sure this port is UP in netdisco (unless it's a lag master,
# because we can still see nodes without a functioning aggregate)
$device_ports->{$port}->update({up_admin => 'up', up => 'up'})
if not $device_ports->{$port}->is_master;
foreach my $mac (keys %{ $fwtable->{$vlan}->{$port} }) {
# remove vlan 0 entry for this MAC addr
delete $fwtable->{0}->{$_}->{$mac}
for keys %{ $fwtable->{0} };
++$total_nodes;
store_node($ip, $vlan, $port, $mac, $now);
}
}
}
debug sprintf ' [%s] macsuck - %s updated forwarding table entries',
$ip, $total_nodes;
# a use for $now ... need to archive dissapeared nodes
my $archived = 0;
if (setting('node_freshness')) {
$archived = schema('netdisco')->resultset('Node')->search({
switch => $ip,
time_last => \[ "< ($now - ?::interval)",
setting('node_freshness') .' minutes' ],
})->update({ active => \'false' });
}
debug sprintf ' [%s] macsuck - removed %d fwd table entries to archive',
$ip, $archived;
$device->update({last_macsuck => \$now});
}
=head2 store_node( $ip, $vlan, $port, $mac, $now? )
Writes a fresh entry to the Netdisco C<node> database table. Will mark old
entries for this data as no longer C<active>.
All four fields in the tuple are required. If you don't know the VLAN ID,
Netdisco supports using ID "0".
Optionally, a fifth argument can be the literal string passed to the time_last
field of the database record. If not provided, it defauls to C<now()>.
=cut
sub store_node {
my ($ip, $vlan, $port, $mac, $now) = @_;
$now ||= 'now()';
$vlan ||= 0;
schema('netdisco')->txn_do(sub {
my $nodes = schema('netdisco')->resultset('Node');
my $old = $nodes->search(
{ mac => $mac,
# where vlan is unknown, need to archive on all other vlans
($vlan ? (vlan => $vlan) : ()),
-bool => 'active',
-not => {
switch => $ip,
port => $port,
},
})->update( { active => \'false' } );
# new data
$nodes->update_or_create(
{
switch => $ip,
port => $port,
vlan => $vlan,
mac => $mac,
active => \'true',
oui => substr($mac,0,8),
time_last => \$now,
(($old != 0) ? (time_recent => \$now) : ()),
},
{
key => 'primary',
for => 'update',
}
);
});
}
# return a list of vlan numbers which are OK to macsuck on this device
sub _get_vlan_list {
my ($device, $snmp) = @_;
return () unless $snmp->cisco_comm_indexing;
my (%vlans, %vlan_names);
my $i_vlan = $snmp->i_vlan || {};
my $trunks = $snmp->i_vlan_membership || {};
my $i_type = $snmp->i_type || {};
# get list of vlans in use
while (my ($idx, $vlan) = each %$i_vlan) {
# hack: if vlan id comes as 1.142 instead of 142
$vlan =~ s/^\d+\.//;
# VLANs are ports interfaces capture VLAN, but don't count as in use
# Port channels are also 'propVirtual', but capture while checking
# trunk VLANs below
if (exists $i_type->{$idx} and $i_type->{$idx} eq 'propVirtual') {
$vlans{$vlan} ||= 0;
}
else {
++$vlans{$vlan};
}
foreach my $t_vlan (@{$trunks->{$idx}}) {
++$vlans{$t_vlan};
}
}
unless (scalar keys %vlans) {
debug sprintf ' [%s] macsuck - no VLANs found.', $device->ip;
return ();
}
my $v_name = $snmp->v_name || {};
# get vlan names (required for config which filters by name)
while (my ($idx, $name) = each %$v_name) {
# hack: if vlan id comes as 1.142 instead of 142
(my $vlan = $idx) =~ s/^\d+\.//;
# just in case i_vlan is different to v_name set
# capture the VLAN, but it's not in use on a port
$vlans{$vlan} ||= 0;
$vlan_names{$vlan} = $name;
}
debug sprintf ' [%s] macsuck - VLANs: %s', $device->ip,
(join ',', sort keys %vlans);
my @ok_vlans = ();
foreach my $vlan (sort keys %vlans) {
my $name = $vlan_names{$vlan} || '(unnamed)';
if (ref [] eq ref setting('macsuck_no_vlan')) {
my $ignore = setting('macsuck_no_vlan');
if ((scalar grep {$_ eq $vlan} @$ignore) or
(scalar grep {$_ eq $name} @$ignore)) {
debug sprintf
' [%s] macsuck VLAN %s - skipped by macsuck_no_vlan config',
$device->ip, $vlan;
next;
}
}
if (ref [] eq ref setting('macsuck_no_devicevlan')) {
my $ignore = setting('macsuck_no_devicevlan');
my $ip = $device->ip;
if ((scalar grep {$_ eq "$ip:$vlan"} @$ignore) or
(scalar grep {$_ eq "$ip:$name"} @$ignore)) {
debug sprintf
' [%s] macsuck VLAN %s - skipped by macsuck_no_devicevlan config',
$device->ip, $vlan;
next;
}
}
if (setting('macsuck_no_unnamed') and $name eq '(unnamed)') {
debug sprintf
' [%s] macsuck VLAN %s - skipped by macsuck_no_unnamed config',
$device->ip, $vlan;
next;
}
if ($vlan == 0 or $vlan > 4094) {
debug sprintf ' [%s] macsuck - invalid VLAN number %s',
$device->ip, $vlan;
next;
}
# check in use by a port on this device
if (!$vlans{$vlan} && !setting('macsuck_all_vlans')) {
debug sprintf
' [%s] macsuck VLAN %s/%s - not in use by any port - skipping.',
$device->ip, $vlan, $name;
next;
}
push @ok_vlans, $vlan;
}
return @ok_vlans;
}
# walks the forwarding table (BRIDGE-MIB) for the device and returns a
# table of node entries.
sub _walk_fwtable {
my ($device, $snmp, $interfaces, $port_macs, $device_ports, $comm_vlan) = @_;
my $skiplist = {}; # ports through which we can see another device
my $cache = {};
my $fw_mac = $snmp->fw_mac;
my $fw_port = $snmp->fw_port;
my $fw_vlan = $snmp->qb_fw_vlan;
my $bp_index = $snmp->bp_index;
# to map forwarding table port to device port we have
# fw_port -> bp_index -> interfaces
while (my ($idx, $mac) = each %$fw_mac) {
my $bp_id = $fw_port->{$idx};
next unless check_mac($device, $mac);
unless (defined $bp_id) {
debug sprintf
' [%s] macsuck %s - %s has no fw_port mapping - skipping.',
$device->ip, $mac, $idx;
next;
}
my $iid = $bp_index->{$bp_id};
unless (defined $iid) {
debug sprintf
' [%s] macsuck %s - port %s has no bp_index mapping - skipping.',
$device->ip, $mac, $bp_id;
next;
}
my $port = $interfaces->{$iid};
unless (defined $port) {
debug sprintf
' [%s] macsuck %s - iid %s has no port mapping - skipping.',
$device->ip, $mac, $iid;
next;
}
if (exists $skiplist->{$port}) {
debug sprintf
' [%s] macsuck %s - seen another device thru port %s - skipping.',
$device->ip, $mac, $port;
next;
}
# this uses the cached $ports resultset to limit hits on the db
my $device_port = $device_ports->{$port};
unless (defined $device_port) {
debug sprintf
' [%s] macsuck %s - port %s is not in database - skipping.',
$device->ip, $mac, $port;
next;
}
my $vlan = $fw_vlan->{$idx} || $comm_vlan || '0';
# check to see if the port is connected to another device
# and if we have that device in the database.
# we have several ways to detect "uplink" port status:
# * a neighbor was discovered using CDP/LLDP
# * a mac addr is seen which belongs to any device port/interface
# * (TODO) admin sets is_uplink_admin on the device_port
# allow to gather MACs on upstream port for some kinds of device that
# do not expose MAC address tables via SNMP. relies on prefetched
# neighbors otherwise it would kill the DB with device lookups.
my $neigh_cannot_macsuck = eval { # can fail
check_device_no($device_port->neighbor, 'macsuck_unsupported') ||
match_devicetype($device_port->remote_type, 'macsuck_unsupported_type') };
if ($device_port->is_uplink) {
if ($neigh_cannot_macsuck) {
debug sprintf
' [%s] macsuck %s - port %s neighbor %s without macsuck support',
$device->ip, $mac, $port,
(eval { $device_port->neighbor->ip }
|| ($device_port->remote_ip
|| $device_port->remote_id || '?'));
# continue!!
}
elsif (my $neighbor = $device_port->neighbor) {
debug sprintf
' [%s] macsuck %s - port %s has neighbor %s - skipping.',
$device->ip, $mac, $port, $neighbor->ip;
next;
}
elsif (my $remote = $device_port->remote_ip) {
debug sprintf
' [%s] macsuck %s - port %s has undiscovered neighbor %s',
$device->ip, $mac, $port, $remote;
# continue!!
}
elsif (not setting('macsuck_bleed')) {
debug sprintf
' [%s] macsuck %s - port %s is detected uplink - skipping.',
$device->ip, $mac, $port;
$skiplist->{$port} = [ $vlan, $mac ] # remember for later
if exists $port_macs->{$mac};
next;
}
}
if (exists $port_macs->{$mac}) {
my $switch_ip = $port_macs->{$mac};
if ($device->ip eq $switch_ip) {
debug sprintf
' [%s] macsuck %s - port %s connects to self - skipping.',
$device->ip, $mac, $port;
next;
}
debug sprintf ' [%s] macsuck %s - port %s is probably an uplink',
$device->ip, $mac, $port;
$device_port->update({is_uplink => \'true'});
# neighbor exists and Netdisco can speak to it, so we don't want
# its MAC address. however don't add to skiplist as that would
# clear all other MACs on the port.
next if $neigh_cannot_macsuck;
# when there's no CDP/LLDP, we only want to gather macs at the
# topology edge, hence skip ports with known device macs.
if (not setting('macsuck_bleed')) {
debug sprintf ' [%s] macsuck %s - adding port %s to skiplist',
$device->ip, $mac, $port;
$skiplist->{$port} = [ $vlan, $mac ]; # remember for later
next;
}
}
# possibly move node to lag master
if (defined $device_port->slave_of
and exists $device_ports->{$device_port->slave_of}) {
$port = $device_port->slave_of;
$device_ports->{$port}->update({is_uplink => \'true'});
}
++$cache->{$vlan}->{$port}->{$mac};
}
# restore MACs of neighbor devices.
# this is when we have a "possible uplink" detected but we still want to
# record the single MAC of the neighbor device so it works in Node search.
foreach my $port (keys %$skiplist) {
my ($vlan, $mac) = @{ $skiplist->{$port} };
delete $cache->{$_}->{$port} for keys %$cache; # nuke nodes on all VLANs
++$cache->{$vlan}->{$port}->{$mac};
}
return $cache;
}
=head2 store_wireless_client_info( $device, $snmp, $now? )
Given a Device database object, and a working SNMP connection, connect to a
device and discover 802.11 related information for all connected wireless
clients.
If the device doesn't support the 802.11 MIBs, then this will silently return.
If the device does support the 802.11 MIBs but Netdisco's configuration
does not permit polling (C<store_wireless_clients> must be true) then a debug
message is logged and the subroutine returns.
Otherwise, client information is gathered and stored to the database.
Optionally, a third argument can be the literal string passed to the time_last
field of the database record. If not provided, it defauls to C<now()>.
=cut
sub store_wireless_client_info {
my ($device, $snmp, $now) = @_;
$now ||= 'now()';
my $cd11_txrate = $snmp->cd11_txrate;
return unless $cd11_txrate and scalar keys %$cd11_txrate;
if (setting('store_wireless_clients')) {
debug sprintf ' [%s] macsuck - gathering wireless client info',
$device->ip;
}
else {
debug sprintf ' [%s] macsuck - dot11 info available but skipped due to config',
$device->ip;
return;
}
my $cd11_rateset = $snmp->cd11_rateset();
my $cd11_uptime = $snmp->cd11_uptime();
my $cd11_sigstrength = $snmp->cd11_sigstrength();
my $cd11_sigqual = $snmp->cd11_sigqual();
my $cd11_mac = $snmp->cd11_mac();
my $cd11_port = $snmp->cd11_port();
my $cd11_rxpkt = $snmp->cd11_rxpkt();
my $cd11_txpkt = $snmp->cd11_txpkt();
my $cd11_rxbyte = $snmp->cd11_rxbyte();
my $cd11_txbyte = $snmp->cd11_txbyte();
my $cd11_ssid = $snmp->cd11_ssid();
while (my ($idx, $txrates) = each %$cd11_txrate) {
my $rates = $cd11_rateset->{$idx};
my $mac = $cd11_mac->{$idx};
next unless defined $mac; # avoid null entries
# there can be more rows in txrate than other tables
my $txrate = defined $txrates->[$#$txrates]
? int($txrates->[$#$txrates])
: undef;
my $maxrate = defined $rates->[$#$rates]
? int($rates->[$#$rates])
: undef;
my $ssid = $cd11_ssid->{$idx} || 'unknown';
schema('netdisco')->txn_do(sub {
schema('netdisco')->resultset('NodeWireless')
->search({ 'me.mac' => $mac, 'me.ssid' => $ssid })
->update_or_create({
txrate => $txrate,
maxrate => $maxrate,
uptime => $cd11_uptime->{$idx},
rxpkt => $cd11_rxpkt->{$idx},
txpkt => $cd11_txpkt->{$idx},
rxbyte => $cd11_rxbyte->{$idx},
txbyte => $cd11_txbyte->{$idx},
sigqual => $cd11_sigqual->{$idx},
sigstrength => $cd11_sigstrength->{$idx},
time_last => \$now,
}, {
order_by => [qw/mac ssid/],
for => 'update',
});
});
}
}
1;

View File

@@ -0,0 +1,179 @@
package App::Netdisco::Core::Nbtstat;
use Dancer qw/:syntax :script/;
use Dancer::Plugin::DBIC 'schema';
use App::Netdisco::Util::Node 'check_mac';
use NetAddr::IP::Lite ':lower';
use App::Netdisco::AnyEvent::Nbtstat;
use Encode;
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = qw/ nbtstat_resolve_async 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 nbtstat_resolve_async( $ips )
This method uses an asynchronous AnyEvent NetBIOS node status requester
C<App::Netdisco::AnyEvent::Nbtstat>.
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 nbtstat_resolve_async {
my $ips = shift;
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;
}
);
}
# Decrement the cv counter to cancel out the send declaration
$cv->end;
# Wait for the resolver to perform all resolutions
$cv->recv;
# Close sockets
undef $stater;
return $ips;
}
# filter nbt names / information
sub _filter_nbname {
my $ip = shift;
my $hash_ref = 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 sprintf ' 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, -bool => 'active' } );
if ( !defined $node_ip ) {
debug sprintf ' no MAC for %s returned by nbtstat or in DB', $ip;
return;
}
$mac = $node_ip->mac;
}
$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?)
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;

31
lib/App/Netdisco/DB.pm Normal file
View File

@@ -0,0 +1,31 @@
use utf8;
package App::Netdisco::DB;
use strict;
use warnings;
use base 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces(
default_resultset_class => 'ResultSet',
);
our # try to hide from kwalitee
$VERSION # try harder
= 41; # schema version used for upgrades, keep as integer
use Path::Class;
use File::Basename;
my (undef, $libpath, undef) = fileparse( $INC{ 'App/Netdisco/DB.pm' } );
our $schema_versions_dir = Path::Class::Dir->new($libpath)
->subdir("DB", "schema_versions")->stringify;
__PACKAGE__->load_components(qw/
Schema::Versioned
+App::Netdisco::DB::ExplicitLocking
/);
__PACKAGE__->upgrade_directory($schema_versions_dir);
1;

View File

@@ -0,0 +1,165 @@
package App::Netdisco::DB::ExplicitLocking;
use strict;
use warnings;
our %lock_modes;
BEGIN {
%lock_modes = (
ACCESS_SHARE => 'ACCESS SHARE',
ROW_SHARE => 'ROW SHARE',
ROW_EXCLUSIVE => 'ROW EXCLUSIVE',
SHARE_UPDATE_EXCLUSIVE => 'SHARE UPDATE EXCLUSIVE',
SHARE => 'SHARE',
SHARE_ROW_EXCLUSIVE => 'SHARE ROW EXCLUSIVE',
EXCLUSIVE => 'EXCLUSIVE',
ACCESS_EXCLUSIVE => 'ACCESS EXCLUSIVE',
);
}
use constant \%lock_modes;
use base 'Exporter';
our @EXPORT = ();
our @EXPORT_OK = (keys %lock_modes);
our %EXPORT_TAGS = (modes => \@EXPORT_OK);
sub txn_do_locked {
my ($self, $table, $mode, $sub) = @_;
my $sql_fmt = q{LOCK TABLE %s IN %%s MODE};
my $schema = $self;
if ($self->can('result_source')) {
# ResultSet component
$sub = $mode;
$mode = $table;
$table = $self->result_source->from;
$schema = $self->result_source->schema;
}
$schema->throw_exception('missing Table name to txn_do_locked()')
unless $table;
$table = [$table] if ref '' eq ref $table;
my $table_fmt = join ', ', ('%s' x scalar @$table);
my $sql = sprintf $sql_fmt, $table_fmt;
if (ref '' eq ref $mode and $mode) {
scalar grep {$_ eq $mode} values %lock_modes
or $schema->throw_exception('bad LOCK_MODE to txn_do_locked()');
}
else {
$sub = $mode;
$mode = 'ACCESS EXCLUSIVE';
}
$schema->txn_do(sub {
my @params = map {$schema->storage->dbh->quote_identifier($_)} @$table;
$schema->storage->dbh->do(sprintf $sql, @params, $mode);
$sub->();
});
}
=head1 NAME
App::Netdisco::DB::ExplicitLocking - Support for PostgreSQL Lock Modes
=head1 SYNOPSIS
In your L<DBIx::Class> schema:
package My::Schema;
__PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');
Then, in your application code:
use App::Netdisco::DB::ExplicitLocking ':modes';
$schema->txn_do_locked($table, MODE_NAME, sub { ... });
This also works for the ResultSet:
package My::Schema::ResultSet::TableName;
__PACKAGE__->load_components('+App::Netdisco::DB::ExplicitLocking');
Then, in your application code:
use App::Netdisco::DB::ExplicitLocking ':modes';
$schema->resultset('TableName')->txn_do_locked(MODE_NAME, sub { ... });
=head1 DESCRIPTION
This L<DBIx::Class> component provides an easy way to execute PostgreSQL table
locks before a transaction block.
You can load the component in either the Schema class or ResultSet class (or
both) and then use an interface very similar to C<DBIx::Class>'s C<txn_do()>.
The package also exports constants for each of the table lock modes supported
by PostgreSQL, which must be used if specifying the mode (default mode is
C<ACCESS EXCLUSIVE>).
=head1 EXPORTS
With the C<:modes> tag (as in SYNOPSIS above) the following constants are
exported and must be used if specifying the lock mode:
=over 4
=item * C<ACCESS_SHARE>
=item * C<ROW_SHARE>
=item * C<ROW_EXCLUSIVE>
=item * C<SHARE_UPDATE_EXCLUSIVE>
=item * C<SHARE>
=item * C<SHARE_ROW_EXCLUSIVE>
=item * C<EXCLUSIVE>
=item * C<ACCESS_EXCLUSIVE>
=back
=head1 METHODS
=head2 C<< $schema->txn_do_locked($table|\@tables, MODE_NAME?, $subref) >>
This is the method signature used when the component is loaded into your
Schema class. The reason you might want to use this over the ResultSet version
(below) is to specify multiple tables to be locked before the transaction.
The first argument is one or more tables, and is required. Note that these are
the real table names in PostgreSQL, and not C<DBIx::Class> ResultSet aliases
or anything like that.
The mode name is optional, and defaults to C<ACCESS EXCLUSIVE>. You must use
one of the exported constants in this parameter.
Finally pass a subroutine reference, just as you would to the normal
C<DBIx::Class> C<txn_do()> method. Note that additional arguments are not
supported.
=head2 C<< $resultset->txn_do_locked(MODE_NAME?, $subref) >>
This is the method signature used when the component is loaded into your
ResultSet class. If you don't yet have a ResultSet class (which is the default
- normally only Result classes are created) then you can create a stub which
simply loads this component (and inherits from C<DBIx::Class::ResultSet>).
This is the simplest way to use this module if you only want to lock one table
before your transaction block.
The first argument is the optional mode name, which defaults to C<ACCESS
EXCLUSIVE>. You must use one of the exported constants in this parameter.
The second argument is a subroutine reference, just as you would pass to the
normal C<DBIx::Class> C<txn_do()> method. Note that additional arguments are
not supported.
=cut
1;

View File

@@ -0,0 +1,117 @@
use utf8;
package App::Netdisco::DB::Result::Admin;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("admin");
__PACKAGE__->add_columns(
"job",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "admin_job_seq",
},
"entered",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"started",
{ data_type => "timestamp", is_nullable => 1 },
"finished",
{ data_type => "timestamp", is_nullable => 1 },
"device",
{ data_type => "inet", is_nullable => 1 },
"port",
{ data_type => "text", is_nullable => 1 },
"action",
{ data_type => "text", is_nullable => 1 },
"subaction",
{ data_type => "text", is_nullable => 1 },
"status",
{ data_type => "text", is_nullable => 1 },
"username",
{ data_type => "text", is_nullable => 1 },
"userip",
{ data_type => "inet", is_nullable => 1 },
"log",
{ data_type => "text", is_nullable => 1 },
"debug",
{ data_type => "boolean", is_nullable => 1 },
);
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:gW4JW4pMgrufFIxFeYPYpw
__PACKAGE__->set_primary_key("job");
# You can replace this text with custom code or comments, and it will be preserved on regeneration
=head1 METHODS
=head2 summary
An attempt to make a meaningful statement about the job.
=cut
sub summary {
my $job = shift;
return join ' ',
$job->action,
($job->device || ''),
($job->port || '');
# ($job->subaction ? (q{'}. $job->subaction .q{'}) : '');
}
=head1 ADDITIONAL COLUMNS
=head2 entererd_stamp
Formatted version of the C<entered> 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 entered_stamp { return (shift)->get_column('entered_stamp') }
=head2 started_stamp
Formatted version of the C<started> 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 started_stamp { return (shift)->get_column('started_stamp') }
=head2 finished_stamp
Formatted version of the C<finished> 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 finished_stamp { return (shift)->get_column('finished_stamp') }
1;

View File

@@ -0,0 +1,21 @@
use utf8;
package App::Netdisco::DB::Result::Community;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("community");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"snmp_comm_rw",
{ data_type => "text", is_nullable => 1 },
"snmp_auth_tag_read",
{ data_type => "text", is_nullable => 1 },
"snmp_auth_tag_write",
{ data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("ip");
1;

View File

@@ -0,0 +1,357 @@
use utf8;
package App::Netdisco::DB::Result::Device;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use NetAddr::IP::Lite ':lower';
use App::Netdisco::Util::DNS 'hostname_from_ip';
use base 'DBIx::Class::Core';
__PACKAGE__->table("device");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"dns",
{ data_type => "text", is_nullable => 1 },
"description",
{ data_type => "text", is_nullable => 1 },
"uptime",
{ data_type => "bigint", is_nullable => 1 },
"contact",
{ data_type => "text", is_nullable => 1 },
"name",
{ data_type => "text", is_nullable => 1 },
"location",
{ data_type => "text", is_nullable => 1 },
"layers",
{ data_type => "varchar", is_nullable => 1, size => 8 },
"ports",
{ data_type => "integer", is_nullable => 1 },
"mac",
{ data_type => "macaddr", is_nullable => 1 },
"serial",
{ data_type => "text", is_nullable => 1 },
"model",
{ data_type => "text", is_nullable => 1 },
"ps1_type",
{ data_type => "text", is_nullable => 1 },
"ps2_type",
{ data_type => "text", is_nullable => 1 },
"ps1_status",
{ data_type => "text", is_nullable => 1 },
"ps2_status",
{ data_type => "text", is_nullable => 1 },
"fan",
{ data_type => "text", is_nullable => 1 },
"slots",
{ data_type => "integer", is_nullable => 1 },
"vendor",
{ data_type => "text", is_nullable => 1 },
"os",
{ data_type => "text", is_nullable => 1 },
"os_ver",
{ data_type => "text", is_nullable => 1 },
"log",
{ data_type => "text", is_nullable => 1 },
"snmp_ver",
{ data_type => "integer", is_nullable => 1 },
"snmp_comm",
{ data_type => "text", is_nullable => 1 },
"snmp_class",
{ data_type => "text", is_nullable => 1 },
"vtp_domain",
{ data_type => "text", is_nullable => 1 },
"last_discover",
{ data_type => "timestamp", is_nullable => 1 },
"last_macsuck",
{ data_type => "timestamp", is_nullable => 1 },
"last_arpnip",
{ data_type => "timestamp", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("ip");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:671/XuuvsO2aMB1+IRWFjg
=head1 RELATIONSHIPS
=head2 device_ips
Returns rows from the C<device_ip> table which relate to this Device. That is,
all the interface IP aliases configured on the Device.
=cut
__PACKAGE__->has_many( device_ips => 'App::Netdisco::DB::Result::DeviceIp', 'ip' );
=head2 vlans
Returns the C<device_vlan> entries for this Device. That is, the list of VLANs
configured on or known by this Device.
=cut
__PACKAGE__->has_many( vlans => 'App::Netdisco::DB::Result::DeviceVlan', 'ip' );
=head2 ports
Returns the set of ports on this Device.
=cut
__PACKAGE__->has_many( ports => 'App::Netdisco::DB::Result::DevicePort', 'ip' );
=head2 modules
Returns the set chassis modules on this Device.
=cut
__PACKAGE__->has_many( modules => 'App::Netdisco::DB::Result::DeviceModule', 'ip' );
=head2 power_modules
Returns the set of power modules on this Device.
=cut
__PACKAGE__->has_many( power_modules => 'App::Netdisco::DB::Result::DevicePower', 'ip' );
=head2 port_vlans
Returns the set of VLANs known to be configured on Ports on this Device,
either tagged or untagged.
The JOIN is of type "RIGHT" meaning that the results are constrained to VLANs
only on Ports on this Device.
=cut
__PACKAGE__->has_many(
port_vlans => 'App::Netdisco::DB::Result::DevicePortVlan',
'ip', { join_type => 'RIGHT' }
);
# helper which assumes we've just RIGHT JOINed to Vlans table
sub vlan { return (shift)->vlans->first }
=head2 wireless_ports
Returns the set of wireless IDs known to be configured on Ports on this
Device.
=cut
__PACKAGE__->has_many(
wireless_ports => 'App::Netdisco::DB::Result::DevicePortWireless',
'ip', { join_type => 'RIGHT' }
);
=head2 ssids
Returns the set of SSIDs known to be configured on Ports on this Device.
=cut
__PACKAGE__->has_many(
ssids => 'App::Netdisco::DB::Result::DevicePortSsid',
'ip', { join_type => 'RIGHT' }
);
=head2 powered_ports
Returns the set of ports known to have PoE capability
=cut
__PACKAGE__->has_many(
powered_ports => 'App::Netdisco::DB::Result::DevicePortPower',
'ip', { join_type => 'RIGHT' }
);
=head2 community
Returns the row from the community string table, if one exists.
=cut
__PACKAGE__->might_have(
community => 'App::Netdisco::DB::Result::Community', 'ip');
=head1 ADDITIONAL METHODS
=head2 renumber( $new_ip )
Will update this device and all related database records to use the new IP
C<$new_ip>. Returns C<undef> if $new_ip seems invalid, otherwise returns the
Device row object.
=cut
sub renumber {
my ($device, $ip) = @_;
my $schema = $device->result_source->schema;
my $new_addr = NetAddr::IP::Lite->new($ip)
or return;
my $old_ip = $device->ip;
my $new_ip = $new_addr->addr;
return
if $new_ip eq '0.0.0.0'
or $new_ip eq '127.0.0.1';
foreach my $set (qw/
DeviceIp
DeviceModule
DevicePower
DeviceVlan
DevicePort
DevicePortLog
DevicePortPower
DevicePortSsid
DevicePortVlan
DevicePortWireless
Community
/) {
$schema->resultset($set)
->search({ip => $old_ip})
->update({ip => $new_ip});
}
$schema->resultset('DevicePort')
->search({remote_ip => $old_ip})
->update({remote_ip => $new_ip});
$schema->resultset('Admin')
->search({device => $old_ip})
->update({device => $new_ip});
$schema->resultset('Node')
->search({switch => $old_ip})
->update({switch => $new_ip});
$schema->resultset('Topology')
->search({dev1 => $old_ip})
->update({dev1 => $new_ip});
$schema->resultset('Topology')
->search({dev2 => $old_ip})
->update({dev2 => $new_ip});
$device->update({
ip => $new_ip,
dns => hostname_from_ip($new_ip),
});
return $device;
}
=head1 ADDITIONAL COLUMNS
=head2 oui
Returns the first half of the device MAC address.
=cut
sub oui { return substr( ((shift)->mac || ''), 0, 8 ) }
=head2 port_count
Returns the number of ports on this device. Enable this
column by applying the C<with_port_count()> modifier to C<search()>.
=cut
sub port_count { return (shift)->get_column('port_count') }
=head2 uptime_age
Formatted version of the C<uptime> field.
The format is in "X days/months/years" style, similar to:
1 year 4 months 05:46:00
=cut
sub uptime_age { return (shift)->get_column('uptime_age') }
=head2 last_discover_stamp
Formatted version of the C<last_discover> 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 last_discover_stamp { return (shift)->get_column('last_discover_stamp') }
=head2 last_macsuck_stamp
Formatted version of the C<last_macsuck> 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 last_macsuck_stamp { return (shift)->get_column('last_macsuck_stamp') }
=head2 last_arpnip_stamp
Formatted version of the C<last_arpnip> 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 last_arpnip_stamp { return (shift)->get_column('last_arpnip_stamp') }
=head2 since_last_discover
Number of seconds which have elapsed since the value of C<last_discover>.
=cut
sub since_last_discover { return (shift)->get_column('since_last_discover') }
=head2 since_last_macsuck
Number of seconds which have elapsed since the value of C<last_macsuck>.
=cut
sub since_last_macsuck { return (shift)->get_column('since_last_macsuck') }
=head2 since_last_arpnip
Number of seconds which have elapsed since the value of C<last_arpnip>.
=cut
sub since_last_arpnip { return (shift)->get_column('since_last_arpnip') }
1;

View File

@@ -0,0 +1,59 @@
use utf8;
package App::Netdisco::DB::Result::DeviceIp;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_ip");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"alias",
{ data_type => "inet", is_nullable => 0 },
"subnet",
{ data_type => "cidr", is_nullable => 1 },
"port",
{ data_type => "text", is_nullable => 1 },
"dns",
{ data_type => "text", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("ip", "alias");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/ugGtBSGyrJ7s6yqJ9bclQ
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table to which this IP alias relates.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 device_port
Returns the Port on which this IP address is configured (typically a loopback,
routed port or virtual interface).
=cut
__PACKAGE__->add_unique_constraint(['alias']);
__PACKAGE__->belongs_to( device_port => 'App::Netdisco::DB::Result::DevicePort',
{ 'foreign.port' => 'self.port', 'foreign.ip' => 'self.ip' } );
1;

View File

@@ -0,0 +1,68 @@
use utf8;
package App::Netdisco::DB::Result::DeviceModule;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_module");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"index",
{ data_type => "integer", is_nullable => 0 },
"description",
{ data_type => "text", is_nullable => 1 },
"type",
{ data_type => "text", is_nullable => 1 },
"parent",
{ data_type => "integer", is_nullable => 1 },
"name",
{ data_type => "text", is_nullable => 1 },
"class",
{ data_type => "text", is_nullable => 1 },
"pos",
{ data_type => "integer", is_nullable => 1 },
"hw_ver",
{ data_type => "text", is_nullable => 1 },
"fw_ver",
{ data_type => "text", is_nullable => 1 },
"sw_ver",
{ data_type => "text", is_nullable => 1 },
"serial",
{ data_type => "text", is_nullable => 1 },
"model",
{ data_type => "text", is_nullable => 1 },
"fru",
{ data_type => "boolean", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"last_discover",
{ data_type => "timestamp", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("ip", "index");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nuwxZBoiip9trdJFmgk3Fw
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table on which this VLAN entry was discovered.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,378 @@
use utf8;
package App::Netdisco::DB::Result::DevicePort;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use NetAddr::MAC;
use MIME::Base64 'encode_base64url';
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_port");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"descr",
{ data_type => "text", is_nullable => 1 },
"up",
{ data_type => "text", is_nullable => 1 },
"up_admin",
{ data_type => "text", is_nullable => 1 },
"type",
{ data_type => "text", is_nullable => 1 },
"duplex",
{ data_type => "text", is_nullable => 1 },
"duplex_admin",
{ data_type => "text", is_nullable => 1 },
"speed",
{ data_type => "text", is_nullable => 1 },
"name",
{ data_type => "text", is_nullable => 1 },
"mac",
{ data_type => "macaddr", is_nullable => 1 },
"mtu",
{ data_type => "integer", is_nullable => 1 },
"stp",
{ data_type => "text", is_nullable => 1 },
"remote_ip",
{ data_type => "inet", is_nullable => 1 },
"remote_port",
{ data_type => "text", is_nullable => 1 },
"remote_type",
{ data_type => "text", is_nullable => 1 },
"remote_id",
{ data_type => "text", is_nullable => 1 },
"is_master",
{ data_type => "bool", is_nullable => 0, default_value => \"false" },
"slave_of",
{ data_type => "text", is_nullable => 1 },
"manual_topo",
{ data_type => "bool", is_nullable => 0, default_value => \"false" },
"is_uplink",
{ data_type => "bool", is_nullable => 1 },
"vlan",
{ data_type => "text", is_nullable => 1 },
"pvid",
{ data_type => "integer", is_nullable => 1 },
"lastchange",
{ data_type => "bigint", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("port", "ip");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:lcbweb0loNwHoWUuxTN/hA
=head1 RELATIONSHIPS
=head2 device
Returns the Device table entry to which the given Port is related.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 port_vlans
Returns the set of C<device_port_vlan> entries associated with this Port.
These will be both tagged and untagged. Use this relation in search conditions.
See also C<all_port_vlans>.
=cut
__PACKAGE__->has_many( port_vlans => 'App::Netdisco::DB::Result::DevicePortVlan',
{ 'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port' } );
=head2 all_port_vlans
Returns the set of C<device_port_vlan> entries associated with this Port.
These will be both tagged and untagged. Use this relation when prefetching related
C<device_port_vlan> rows.
See also C<port_vlans>.
=cut
__PACKAGE__->has_many( all_port_vlans => 'App::Netdisco::DB::Result::DevicePortVlan',
{ 'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port' } );
=head2 nodes / active_nodes / nodes_with_age / active_nodes_with_age
Returns the set of Nodes whose MAC addresses are associated with this Device
Port.
The C<active> variants return only the subset of nodes currently in the switch
MAC address table, that is the active ones.
The C<with_age> variants add an additional column C<time_last_age>, a
preformatted value for the Node's C<time_last> field, which reads as "X
days/weeks/months/years".
=cut
__PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node',
{
'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port',
},
{ join_type => 'LEFT' },
);
__PACKAGE__->has_many( nodes_with_age => 'App::Netdisco::DB::Result::Virtual::NodeWithAge',
{
'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port',
},
{ join_type => 'LEFT',
cascade_copy => 0, cascade_update => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many( active_nodes => 'App::Netdisco::DB::Result::Virtual::ActiveNode',
{
'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port',
},
{ join_type => 'LEFT',
cascade_copy => 0, cascade_update => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many( active_nodes_with_age => 'App::Netdisco::DB::Result::Virtual::ActiveNodeWithAge',
{
'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port',
},
{ join_type => 'LEFT',
cascade_copy => 0, cascade_update => 0, cascade_delete => 0 },
);
=head2 logs
Returns the set of C<device_port_log> entries associated with this Port.
=cut
__PACKAGE__->has_many( logs => 'App::Netdisco::DB::Result::DevicePortLog',
{ 'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port' },
);
=head2 power
Returns a row from the C<device_port_power> table if one refers to this
device port.
=cut
__PACKAGE__->might_have( power => 'App::Netdisco::DB::Result::DevicePortPower', {
'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port',
});
=head2 ssid
Returns a row from the C<device_port_ssid> table if one refers to this
device port.
=cut
__PACKAGE__->might_have(
ssid => 'App::Netdisco::DB::Result::DevicePortSsid',
{ 'foreign.ip' => 'self.ip',
'foreign.port' => 'self.port',
}
);
=head2 wireless
Returns a row from the C<device_port_wireless> table if one refers to this
device port.
=cut
__PACKAGE__->might_have(
wireless => 'App::Netdisco::DB::Result::DevicePortWireless',
{ 'foreign.ip' => 'self.ip',
'foreign.port' => 'self.port',
}
);
=head2 agg_master
Returns another row from the C<device_port> table if this port is slave
to another in a link aggregate.
=cut
__PACKAGE__->belongs_to(
agg_master => 'App::Netdisco::DB::Result::DevicePort', {
'foreign.ip' => 'self.ip',
'foreign.port' => 'self.slave_of',
}, {
join_type => 'LEFT',
}
);
=head2 neighbor_alias
When a device port has an attached neighbor device, this relationship will
return the IP address of the neighbor. See the C<neighbor> helper method if
what you really want is to retrieve the Device entry for that neighbor.
The JOIN is of type "LEFT" in case the neighbor device is known but has not
been fully discovered by Netdisco and so does not exist itself in the
database.
=cut
__PACKAGE__->belongs_to( neighbor_alias => 'App::Netdisco::DB::Result::DeviceIp',
sub {
my $args = shift;
return {
"$args->{foreign_alias}.ip" => { '=' =>
$args->{self_resultsource}->schema->resultset('DeviceIp')
->search({alias => { -ident => "$args->{self_alias}.remote_ip"}},
{rows => 1, columns => 'ip', alias => 'devipsub'})->as_query }
};
},
{ join_type => 'LEFT' },
);
=head2 vlans
As compared to C<port_vlans>, this relationship returns a set of VLAN
row objects for the VLANs on the given port, which might be more useful if you
want to find out details such as the VLAN name.
See also C<vlan_count>.
=cut
__PACKAGE__->many_to_many( vlans => 'all_port_vlans', 'vlan' );
=head2 oui
Returns the C<oui> table entry matching this Port. 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' }
);
=head1 ADDITIONAL METHODS
=head2 neighbor
Returns the Device entry for the neighbour Device on the given port.
Might return an undefined value if there is no neighbor on the port, or if the
neighbor has not been fully discovered by Netdisco and so does not exist in
the database.
=cut
sub neighbor {
my $row = shift;
return eval { $row->neighbor_alias->device || undef };
}
=head1 ADDITIONAL COLUMNS
=head2 native
An alias for the C<vlan> column, which stores the PVID (that is, the VLAN
ID assigned to untagged frames received on the port).
=cut
sub native { return (shift)->vlan }
=head2 vlan_count
Returns the number of VLANs active on this device port. Enable this column by
applying the C<with_vlan_count()> modifier to C<search()>.
=cut
sub vlan_count { return (shift)->get_column('vlan_count') }
=head2 lastchange_stamp
Formatted version of the C<lastchange> field, accurate to the minute. Enable
this column by applying the C<with_times()> modifier to C<search()>.
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 lastchange_stamp { return (shift)->get_column('lastchange_stamp') }
=head2 is_free
This method can be used to evaluate whether a device port could be considered
unused, based on the last time it changed from the "up" state to a "down"
state.
See the C<with_is_free> and C<only_free_ports> modifiers to C<search()>.
=cut
sub is_free { return (shift)->get_column('is_free') }
=head2 base64url_port
Returns a Base64 encoded version of the C<port> column value suitable for use
in a URL.
=cut
sub base64url_port { return encode_base64url((shift)->port) }
=head2 net_mac
Returns the C<mac> column instantiated into a L<NetAddr::MAC> object.
=cut
sub net_mac { return NetAddr::MAC->new(mac => (shift)->mac) }
=head2 last_comment
Returns the most recent comment from the logs for this device port.
=cut
sub last_comment {
my $row = (shift)->logs->search(undef,
{ order_by => { -desc => 'creation' }, rows => 1 })->first;
return ($row ? $row->log : '');
}
1;

View File

@@ -0,0 +1,60 @@
use utf8;
package App::Netdisco::DB::Result::DevicePortLog;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_port_log");
__PACKAGE__->add_columns(
"id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "device_port_log_id_seq",
},
"ip",
{ data_type => "inet", is_nullable => 1 },
"port",
{ data_type => "text", is_nullable => 1 },
"reason",
{ data_type => "text", is_nullable => 1 },
"log",
{ data_type => "text", is_nullable => 1 },
"username",
{ data_type => "text", is_nullable => 1 },
"userip",
{ data_type => "inet", is_nullable => 1 },
"action",
{ data_type => "text", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("id");
=head1 ADDITIONAL COLUMNS
=head2 creation_stamp
Formatted version of the C<creation> field, accurate to the second.
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:23
=cut
sub creation_stamp { return (shift)->get_column('creation_stamp') }
1;

View File

@@ -0,0 +1,57 @@
use utf8;
package App::Netdisco::DB::Result::DevicePortPower;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_port_power");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"module",
{ data_type => "integer", is_nullable => 1 },
"admin",
{ data_type => "text", is_nullable => 1 },
"status",
{ data_type => "text", is_nullable => 1 },
"class",
{ data_type => "text", is_nullable => 1 },
"power",
{ data_type => "integer", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("port", "ip");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sHcdItRUFUOAtIZQjdWbcg
=head1 RELATIONSHIPS
=head2 port
Returns the entry from the C<port> table for which this Power entry applies.
=cut
__PACKAGE__->belongs_to( port => 'App::Netdisco::DB::Result::DevicePort', {
'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port',
});
=head2 device_module
Returns the entry from the C<device_power> table for which this Power entry
applies.
=cut
__PACKAGE__->belongs_to( device_module => 'App::Netdisco::DB::Result::DevicePower', {
'foreign.ip' => 'self.ip', 'foreign.module' => 'self.module',
});
1;

View File

@@ -0,0 +1,67 @@
use utf8;
package App::Netdisco::DB::Result::DevicePortSsid;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_port_ssid");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"ssid",
{ data_type => "text", is_nullable => 1 },
"broadcast",
{ data_type => "boolean", is_nullable => 1 },
"bssid",
{ data_type => "macaddr", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("port", "ip");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zvgylKzUQtizJZCe1rEdUg
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table which hosts this SSID.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 port
Returns the entry from the C<port> table which corresponds to this SSID.
=cut
__PACKAGE__->belongs_to( port => 'App::Netdisco::DB::Result::DevicePort', {
'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port',
});
=head2 nodes
Returns the set of Nodes whose MAC addresses are associated with this Device
Port SSID.
=cut
__PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node',
{
'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port',
},
{ join_type => 'LEFT',
cascade_copy => 0, cascade_update => 0, cascade_delete => 0 },
);
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,76 @@
use utf8;
package App::Netdisco::DB::Result::DevicePortVlan;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_port_vlan");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"vlan",
{ data_type => "integer", is_nullable => 0 },
"native",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"last_discover",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"vlantype",
{ data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("ip", "port", "vlan", "native");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/3KLjJ3D18pGaPEaw9EU5w
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table which hosts the Port on which this
VLAN is configured.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 port
Returns the entry from the C<port> table on which this VLAN is configured.
=cut
__PACKAGE__->belongs_to( port => 'App::Netdisco::DB::Result::DevicePort', {
'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port',
});
=head2 vlan
Returns the entry from the C<device_vlan> table describing this VLAN in
detail, typically in order that the C<name> can be retrieved.
=cut
__PACKAGE__->belongs_to( vlan => 'App::Netdisco::DB::Result::DeviceVlan', {
'foreign.ip' => 'self.ip', 'foreign.vlan' => 'self.vlan',
});
1;

View File

@@ -0,0 +1,66 @@
use utf8;
package App::Netdisco::DB::Result::DevicePortWireless;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_port_wireless");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"channel",
{ data_type => "integer", is_nullable => 1 },
"power",
{ data_type => "integer", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("port", "ip");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:T5GmnCj/9BB7meiGZ3xN7g
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table which hosts this wireless port.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 port
Returns the entry from the C<port> table which corresponds to this wireless
interface.
=cut
__PACKAGE__->belongs_to( port => 'App::Netdisco::DB::Result::DevicePort', {
'foreign.ip' => 'self.ip', 'foreign.port' => 'self.port',
});
=head2 nodes
Returns the set of Nodes whose MAC addresses are associated with this Device
Port Wireless.
=cut
__PACKAGE__->has_many( nodes => 'App::Netdisco::DB::Result::Node',
{
'foreign.switch' => 'self.ip',
'foreign.port' => 'self.port',
},
{ join_type => 'LEFT',
cascade_copy => 0, cascade_update => 0, cascade_delete => 0 },
);
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,49 @@
use utf8;
package App::Netdisco::DB::Result::DevicePower;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_power");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"module",
{ data_type => "integer", is_nullable => 0 },
"power",
{ data_type => "integer", is_nullable => 1 },
"status",
{ data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("ip", "module");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:awZRI/IH2VewzGlxISsr7w
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table on which this power module was discovered.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 ports
Returns the set of PoE ports associated with a power module.
=cut
__PACKAGE__->has_many( ports => 'App::Netdisco::DB::Result::DevicePortPower', {
'foreign.ip' => 'self.ip', 'foreign.module' => 'self.module',
} );
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,42 @@
use utf8;
package App::Netdisco::DB::Result::DeviceRoute;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_route");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"network",
{ data_type => "cidr", is_nullable => 0 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"dest",
{ data_type => "inet", is_nullable => 0 },
"last_discover",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("ip", "network", "dest");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3jcvPP60E5BvwnUbXql7mQ
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,113 @@
use utf8;
package App::Netdisco::DB::Result::DeviceVlan;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("device_vlan");
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"vlan",
{ data_type => "integer", is_nullable => 0 },
"description",
{ data_type => "text", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"last_discover",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("ip", "vlan");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:hBJRcdzOic4d3u4pD1m8iA
=head1 RELATIONSHIPS
=head2 device
Returns the entry from the C<device> table on which this VLAN entry was discovered.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device', 'ip' );
=head2 port_vlans_tagged
Link relationship for C<tagged_ports>, see below.
=cut
__PACKAGE__->has_many( port_vlans_tagged => 'App::Netdisco::DB::Result::DevicePortVlan',
sub {
my $args = shift;
return {
"$args->{foreign_alias}.ip" => { -ident => "$args->{self_alias}.ip" },
"$args->{foreign_alias}.vlan" => { -ident => "$args->{self_alias}.vlan" },
-not_bool => "$args->{foreign_alias}.native",
};
},
{ cascade_copy => 0, cascade_update => 0, cascade_delete => 0 }
);
=head2 port_vlans_untagged
Link relationship to support C<untagged_ports>, see below.
=cut
__PACKAGE__->has_many( port_vlans_untagged => 'App::Netdisco::DB::Result::DevicePortVlan',
sub {
my $args = shift;
return {
"$args->{foreign_alias}.ip" => { -ident => "$args->{self_alias}.ip" },
"$args->{foreign_alias}.vlan" => { -ident => "$args->{self_alias}.vlan" },
-bool => "$args->{foreign_alias}.native",
};
},
{ cascade_copy => 0, cascade_update => 0, cascade_delete => 0 }
);
=head2 ports
Link relationship to support C<ports>.
=cut
__PACKAGE__->has_many( ports => 'App::Netdisco::DB::Result::DevicePortVlan',
{ 'foreign.ip' => 'self.ip', 'foreign.vlan' => 'self.vlan' },
{ cascade_copy => 0, cascade_update => 0, cascade_delete => 0 }
);
=head2 tagged_ports
Returns the set of Device Ports on which this VLAN is configured to be tagged.
=cut
__PACKAGE__->many_to_many( tagged_ports => 'port_vlans_tagged', 'port' );
=head2 untagged_ports
Returns the set of Device Ports on which this VLAN is an untagged VLAN.
=cut
__PACKAGE__->many_to_many( untagged_ports => 'port_vlans_untagged', 'port' );
1;

View File

@@ -0,0 +1,41 @@
use utf8;
package App::Netdisco::DB::Result::Log;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("log");
__PACKAGE__->add_columns(
"id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "log_id_seq",
},
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"class",
{ data_type => "text", is_nullable => 1 },
"entry",
{ data_type => "text", is_nullable => 1 },
"logfile",
{ data_type => "text", is_nullable => 1 },
);
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:eonwOHvvzWm88Ug+IGKuzg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,202 @@
use utf8;
package App::Netdisco::DB::Result::Node;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use NetAddr::MAC;
use base 'DBIx::Class::Core';
__PACKAGE__->table("node");
__PACKAGE__->add_columns(
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"switch",
{ data_type => "inet", is_nullable => 0 },
"port",
{ data_type => "text", is_nullable => 0 },
"active",
{ data_type => "boolean", is_nullable => 1 },
"oui",
{ data_type => "varchar", is_nullable => 1, size => 8 },
"time_first",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"time_recent",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"time_last",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"vlan",
{ data_type => "text", is_nullable => 0, default_value => '0' },
);
__PACKAGE__->set_primary_key("mac", "switch", "port", "vlan");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:sGGyKEfUkoIFVtmj1wnH7A
=head1 RELATIONSHIPS
=head2 device
Returns the single C<device> to which this Node entry was associated at the
time of discovery.
The JOIN is of type LEFT, in case the C<device> is no longer present in the
database but the relation is being used in C<search()>.
=cut
__PACKAGE__->belongs_to( device => 'App::Netdisco::DB::Result::Device',
{ 'foreign.ip' => 'self.switch' }, { join_type => 'LEFT' } );
=head2 device_port
Returns the single C<device_port> to which this Node entry was associated at
the time of discovery.
The JOIN is of type LEFT, in case the C<device> is no longer present in the
database but the relation is being used in C<search()>.
=cut
# device port may have been deleted (reconfigured modules?) but node remains
__PACKAGE__->belongs_to( device_port => 'App::Netdisco::DB::Result::DevicePort',
{ 'foreign.ip' => 'self.switch', 'foreign.port' => 'self.port' },
{ join_type => 'LEFT' }
);
=head2 wireless_port
Returns the single C<wireless_port> to which this Node entry was associated at
the time of discovery.
The JOIN is of type LEFT, in case the C<device> is no longer present in the
database but the relation is being used in C<search()>.
=cut
__PACKAGE__->belongs_to(
wireless_port => 'App::Netdisco::DB::Result::DevicePortWireless',
{ 'foreign.ip' => 'self.switch', 'foreign.port' => 'self.port' },
{ join_type => 'LEFT' }
);
=head2 ips
Returns the set of C<node_ip> entries associated with this Node. That is, the
IP addresses which this MAC address was hosting at the time of discovery.
Note that the Active status of the returned IP entries will all be the same as
the current Node's.
=cut
__PACKAGE__->has_many( ips => 'App::Netdisco::DB::Result::NodeIp',
{ 'foreign.mac' => 'self.mac', 'foreign.active' => 'self.active' } );
=head2 ip4s
Same as C<ips> but for IPv4 only.
=cut
__PACKAGE__->has_many( ip4s => 'App::Netdisco::DB::Result::Virtual::NodeIp4',
{ 'foreign.mac' => 'self.mac', 'foreign.active' => 'self.active' } );
=head2 ip6s
Same as C<ips> but for IPv6 only.
=cut
__PACKAGE__->has_many( ip6s => 'App::Netdisco::DB::Result::Virtual::NodeIp6',
{ 'foreign.mac' => 'self.mac', 'foreign.active' => 'self.active' } );
=head2 netbios
Returns the C<node_nbt> entry associated with this Node if one exists. That
is, the NetBIOS information of this MAC address at the time of discovery.
=cut
__PACKAGE__->might_have( netbios => 'App::Netdisco::DB::Result::NodeNbt',
{ 'foreign.mac' => 'self.mac' } );
=head2 wireless
Returns the set of C<node_wireless> entries associated with this Node. That
is, the SSIDs and wireless statistics associated with this MAC address
at the time of discovery.
=cut
__PACKAGE__->has_many( wireless => 'App::Netdisco::DB::Result::NodeWireless',
{ 'foreign.mac' => 'self.mac' } );
=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', 'oui',
{ join_type => 'LEFT' } );
=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<NetAddr::MAC> object.
=cut
sub net_mac { return NetAddr::MAC->new(mac => (shift)->mac) }
1;

View File

@@ -0,0 +1,230 @@
use utf8;
package App::Netdisco::DB::Result::NodeIp;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use NetAddr::MAC;
use base 'DBIx::Class::Core';
__PACKAGE__->table("node_ip");
__PACKAGE__->add_columns(
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"ip",
{ data_type => "inet", is_nullable => 0 },
"dns",
{ data_type => "text", is_nullable => 1 },
"active",
{ data_type => "boolean", is_nullable => 1 },
"time_first",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"time_last",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("mac", "ip");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9+CuvuVWH88WxAf6IBij8g
=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 node_ips
Returns the set of all C<node_ip> entries which are associated together with
this IP. That is, all the IP addresses hosted on the same interface (MAC
address) as the current Node IP entry.
Note that the set will include the original Node IP object itself. If you wish
to find the I<other> IPs excluding this one, see the C<ip_aliases> helper
routine, below.
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.
=cut
__PACKAGE__->has_many( node_ips => 'App::Netdisco::DB::Result::NodeIp',
{ 'foreign.mac' => 'self.mac' } );
=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_ip> 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 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' => {
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 ip_aliases( \%cond, \%attrs? )
Returns the set of other C<node_ip> entries hosted on the same interface (MAC
address) as the current Node IP, excluding the current IP itself.
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.
=back
=cut
sub ip_aliases {
my ($row, $cond, $attrs) = @_;
my $rs = $row->node_ips({ip => { '!=' => $row->ip }});
return $rs
->search_rs({}, $search_attr)
->search($cond, $attrs);
}
=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<NetAddr::MAC> object.
=cut
sub net_mac { return NetAddr::MAC->new(mac => (shift)->mac) }
1;

View File

@@ -0,0 +1,37 @@
use utf8;
package App::Netdisco::DB::Result::NodeMonitor;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("node_monitor");
__PACKAGE__->add_columns(
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"active",
{ data_type => "boolean", is_nullable => 1 },
"why",
{ data_type => "text", is_nullable => 1 },
"cc",
{ data_type => "text", is_nullable => 1 },
"date",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__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:0prRdz2XYlFuE+nahsI2Yg
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,187 @@
use utf8;
package App::Netdisco::DB::Result::NodeNbt;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use NetAddr::MAC;
use base 'DBIx::Class::Core';
__PACKAGE__->table("node_nbt");
__PACKAGE__->add_columns(
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"ip",
{ data_type => "inet", is_nullable => 1 },
"nbname",
{ data_type => "text", is_nullable => 1 },
"domain",
{ data_type => "text", is_nullable => 1 },
"server",
{ data_type => "boolean", is_nullable => 1 },
"nbuser",
{ data_type => "text", is_nullable => 1 },
"active",
{ data_type => "boolean", is_nullable => 1 },
"time_first",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"time_last",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__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<NetAddr::MAC> object.
=cut
sub net_mac { return NetAddr::MAC->new(mac => (shift)->mac) }
1;

View File

@@ -0,0 +1,96 @@
use utf8;
package App::Netdisco::DB::Result::NodeWireless;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use NetAddr::MAC;
use base 'DBIx::Class::Core';
__PACKAGE__->table("node_wireless");
__PACKAGE__->add_columns(
"mac",
{ data_type => "macaddr", is_nullable => 0 },
"uptime",
{ data_type => "integer", is_nullable => 1 },
"maxrate",
{ data_type => "integer", is_nullable => 1 },
"txrate",
{ data_type => "integer", is_nullable => 1 },
"sigstrength",
{ data_type => "integer", is_nullable => 1 },
"sigqual",
{ data_type => "integer", is_nullable => 1 },
"rxpkt",
{ data_type => "integer", is_nullable => 1 },
"txpkt",
{ data_type => "integer", is_nullable => 1 },
"rxbyte",
{ data_type => "bigint", is_nullable => 1 },
"txbyte",
{ data_type => "bigint", is_nullable => 1 },
"time_last",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"ssid",
{ data_type => "text", is_nullable => 0, default_value => '' },
);
__PACKAGE__->set_primary_key("mac", "ssid");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3xsSiWzL85ih3vhdews8Hg
=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 node
Returns the C<node> table entry matching this wireless entry.
The JOIN is of type LEFT, in case the C<node> is no longer present in the
database but the relation is being used in C<search()>.
=cut
__PACKAGE__->belongs_to( node => 'App::Netdisco::DB::Result::Node',
{ 'foreign.mac' => 'self.mac' },
{ join_type => 'LEFT' } );
=head1 ADDITIONAL COLUMNS
=head2 net_mac
Returns the C<mac> column instantiated into a L<NetAddr::MAC> object.
=cut
sub net_mac { return NetAddr::MAC->new(mac => (shift)->mac) }
1;

View File

@@ -0,0 +1,28 @@
use utf8;
package App::Netdisco::DB::Result::Oui;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("oui");
__PACKAGE__->add_columns(
"oui",
{ data_type => "varchar", is_nullable => 0, size => 8 },
"company",
{ data_type => "text", is_nullable => 1 },
"abbrev",
{ data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("oui");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:s51mj6SvstPd4GdNEy9SoA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,38 @@
use utf8;
package App::Netdisco::DB::Result::Process;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("process");
__PACKAGE__->add_columns(
"controller",
{ data_type => "integer", is_nullable => 0 },
"device",
{ data_type => "inet", is_nullable => 0 },
"action",
{ data_type => "text", is_nullable => 0 },
"status",
{ data_type => "text", is_nullable => 1 },
"count",
{ data_type => "integer", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:28hTnOo4oNwJabiWWHBgCw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,33 @@
use utf8;
package App::Netdisco::DB::Result::Session;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("sessions");
__PACKAGE__->add_columns(
"id",
{ data_type => "char", is_nullable => 0, size => 32 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"a_session",
{ data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:khNPh72VjQh8QHayuW/p1w
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,38 @@
use utf8;
package App::Netdisco::DB::Result::Subnet;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("subnets");
__PACKAGE__->add_columns(
"net",
{ data_type => "cidr", is_nullable => 0 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"last_discover",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
__PACKAGE__->set_primary_key("net");
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:1EHOfYx8PYOHoTkViZR6OA
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,35 @@
use utf8;
package App::Netdisco::DB::Result::Topology;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("topology");
__PACKAGE__->add_columns(
"dev1",
{ data_type => "inet", is_nullable => 0 },
"port1",
{ data_type => "text", is_nullable => 0 },
"dev2",
{ data_type => "inet", is_nullable => 0 },
"port2",
{ data_type => "text", is_nullable => 0 },
);
__PACKAGE__->add_unique_constraint(['dev1','port1']);
__PACKAGE__->add_unique_constraint(['dev2','port2']);
__PACKAGE__->belongs_to(
device1 => 'App::Netdisco::DB::Result::Device',
{'foreign.ip' => 'self.dev1'}
);
__PACKAGE__->belongs_to(
device2 => 'App::Netdisco::DB::Result::Device',
{'foreign.ip' => 'self.dev2'}
);
1;

View File

@@ -0,0 +1,45 @@
use utf8;
package App::Netdisco::DB::Result::User;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("users");
__PACKAGE__->add_columns(
"username",
{ data_type => "varchar", is_nullable => 0, size => 50 },
"password",
{ data_type => "text", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
"last_on",
{ data_type => "timestamp", is_nullable => 1 },
"port_control",
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
"ldap",
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
"admin",
{ data_type => "boolean", default_value => \"false", is_nullable => 1 },
"fullname",
{ data_type => "text", is_nullable => 1 },
"note",
{ data_type => "text", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("username");
__PACKAGE__->has_many( roles => 'App::Netdisco::DB::Result::Virtual::UserRole',
'username', { cascade_copy => 0, cascade_update => 0, cascade_delete => 0 } );
sub created { return (shift)->get_column('created') }
sub last_seen { return (shift)->get_column('last_seen') }
1;

View File

@@ -0,0 +1,43 @@
use utf8;
package App::Netdisco::DB::Result::UserLog;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table("user_log");
__PACKAGE__->add_columns(
"entry",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "user_log_entry_seq",
},
"username",
{ data_type => "varchar", is_nullable => 1, size => 50 },
"userip",
{ data_type => "inet", is_nullable => 1 },
"event",
{ data_type => "text", is_nullable => 1 },
"details",
{ data_type => "text", is_nullable => 1 },
"creation",
{
data_type => "timestamp",
default_value => \"current_timestamp",
is_nullable => 1,
original => { default_value => \"now()" },
},
);
# Created by DBIx::Class::Schema::Loader v0.07015 @ 2012-01-07 14:20:02
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BFrhjYJOhcLIHeWviu9rjw
# You can replace this text with custom code or comments, and it will be preserved on regeneration
1;

View File

@@ -0,0 +1,19 @@
use utf8;
package App::Netdisco::DB::Result::Virtual::ActiveNode;
use strict;
use warnings;
use base 'App::Netdisco::DB::Result::Node';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("active_node");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{
SELECT * FROM node WHERE active
});
1;

View File

@@ -0,0 +1,27 @@
use utf8;
package App::Netdisco::DB::Result::Virtual::ActiveNodeWithAge;
use strict;
use warnings;
use base 'App::Netdisco::DB::Result::Virtual::ActiveNode';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("active_node_with_age");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{
SELECT *,
replace( date_trunc( 'minute', age( now(), time_last + interval '30 second' ) ) ::text, 'mon', 'month')
AS time_last_age
FROM node WHERE active
});
__PACKAGE__->add_columns(
"time_last_age",
{ data_type => "text", is_nullable => 1 },
);
1;

View File

@@ -0,0 +1,71 @@
package App::Netdisco::DB::Result::Virtual::ApRadioChannelPower;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('ap_radio_channel_power');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT w.channel,
w.power,
w.ip,
w.port,
dp.name AS port_name,
dp.descr,
d.name AS device_name,
d.dns,
d.model,
d.location,
CASE
WHEN w.power > 0 THEN round((10.0 * log(w.power) / log(10))::numeric, 1)
ELSE NULL
END AS power2
FROM device_port_wireless AS w
JOIN device_port AS dp ON dp.port = w.port
AND dp.ip = w.ip
JOIN device AS d ON d.ip = w.ip
WHERE w.channel != '0'
ENDSQL
);
__PACKAGE__->add_columns(
'channel' => {
data_type => 'integer',
},
'power' => {
data_type => 'integer',
},
'ip' => {
data_type => 'inet',
},
'port' => {
data_type => 'text',
},
'port_name' => {
data_type => 'text',
},
'descr' => {
data_type => 'text',
},
'device_name' => {
data_type => 'text',
},
'dns' => {
data_type => 'text',
},
'model' => {
data_type => 'text',
},
'location' => {
data_type => 'text',
},
'power2' => {
data_type => 'numeric',
},
);
1;

View File

@@ -0,0 +1,50 @@
package App::Netdisco::DB::Result::Virtual::CidrIps;
use strict;
use warnings;
use utf8;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('cidr_ips');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT host(network (prefix) + sub.int)::inet AS ip,
NULL AS mac,
NULL::text AS dns,
NULL::timestamp AS time_first,
NULL::timestamp AS time_last,
false::boolean AS active
FROM (
SELECT prefix,
generate_series(1, (broadcast(prefix) - network(prefix) - 1)) AS int
FROM (
SELECT ?::inet AS prefix
) AS addr
) AS sub
ENDSQL
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"mac",
{ data_type => "macaddr", is_nullable => 1 },
"dns",
{ data_type => "text", is_nullable => 1 },
"active",
{ data_type => "boolean", is_nullable => 1 },
"time_first",
{
data_type => "timestamp",
is_nullable => 1,
},
"time_last",
{
data_type => "timestamp",
is_nullable => 1,
},
);
1;

View File

@@ -0,0 +1,24 @@
package App::Netdisco::DB::Result::Virtual::DeviceDnsMismatch;
use strict;
use warnings;
use utf8;
use base 'App::Netdisco::DB::Result::Device';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('device_dns_mismatch');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT *
FROM device
WHERE dns IS NULL
OR name IS NULL
OR regexp_replace(lower(dns), ? || '$', '')
!= regexp_replace(lower(name), ? || '$', '')
ENDSQL
1;

View File

@@ -0,0 +1,48 @@
package App::Netdisco::DB::Result::Virtual::DeviceLinks;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('device_links');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT dp.ip AS left_ip, dp.port AS left_port, di.ip AS right_ip, dp.remote_port AS right_port
FROM ( SELECT device_port.ip, device_port.port, device_port.remote_ip, device_port.remote_port
FROM device_port
WHERE device_port.remote_port IS NOT NULL
GROUP BY device_port.ip, device_port.port, device_port.remote_ip, device_port.remote_port
ORDER BY device_port.ip) dp
LEFT JOIN device_ip di ON dp.remote_ip = di.alias
WHERE di.ip IS NOT NULL
ORDER BY dp.ip
ENDSQL
);
__PACKAGE__->add_columns(
'left_ip' => {
data_type => 'inet',
},
'left_port' => {
data_type => 'text',
},
'right_ip' => {
data_type => 'inet',
},
'right_port' => {
data_type => 'text',
},
);
__PACKAGE__->has_many('left_vlans', 'App::Netdisco::DB::Result::DevicePortVlan',
{ 'foreign.ip' => 'self.left_ip', 'foreign.port' => 'self.left_port' },
{ join_type => 'INNER' } );
__PACKAGE__->has_many('right_vlans', 'App::Netdisco::DB::Result::DevicePortVlan',
{ 'foreign.ip' => 'self.right_ip', 'foreign.port' => 'self.right_port' },
{ join_type => 'INNER' } );
1;

View File

@@ -0,0 +1,87 @@
package App::Netdisco::DB::Result::Virtual::DevicePoeStatus;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('device_poe_status');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT DISTINCT ON (dp.ip,dp.module)
dp.ip,
dp.module,
dp.power::bigint,
dp.status,
d.dns,
d.name,
d.model,
d.location,
COUNT(dpp.port) OVER (PARTITION BY dp.ip, dp.module) AS poe_capable_ports,
SUM(CASE WHEN dpp.status = 'deliveringPower' THEN 1 ELSE 0 END) OVER (PARTITION BY dp.ip, dp.module) AS poe_powered_ports,
SUM(CASE WHEN dpp.admin = 'false' THEN 1 ELSE 0 END) OVER (PARTITION BY dp.ip, dp.module) AS poe_disabled_ports,
SUM(CASE WHEN dpp.status ILIKE '%fault' THEN 1
ELSE 0 END) OVER (PARTITION BY dp.ip, dp.module) AS poe_errored_ports,
SUM(CASE WHEN dpp.status = 'deliveringPower' AND dpp.class = 'class4' THEN 30.0
WHEN dpp.status = 'deliveringPower' AND dpp.class = 'class2' THEN 7.0
WHEN dpp.status = 'deliveringPower' AND dpp.class = 'class1' THEN 4.0
WHEN dpp.status = 'deliveringPower' AND dpp.class = 'class3' THEN 15.4
WHEN dpp.status = 'deliveringPower' AND dpp.class = 'class0' THEN 15.4
WHEN dpp.status = 'deliveringPower' AND dpp.class IS NULL THEN 15.4
ELSE 0 END) OVER (PARTITION BY dp.ip, dp.module) AS poe_power_committed,
SUM(CASE WHEN (dpp.power IS NULL OR dpp.power = '0') THEN 0
ELSE round(dpp.power/1000.0, 1) END) OVER (PARTITION BY dp.ip, dp.module) AS poe_power_delivering
FROM device_power dp
JOIN device_port_power dpp ON dpp.ip = dp.ip
AND dpp.module = dp.module
JOIN device d ON dp.ip = d.ip
ENDSQL
__PACKAGE__->add_columns(
'ip' => {
data_type => 'inet',
},
'module' => {
data_type => 'integer',
},
'power' => {
data_type => 'integer',
},
'status' => {
data_type => 'text',
},
'dns' => {
data_type => 'text',
},
'name' => {
data_type => 'text',
},
'model' => {
data_type => 'text',
},
'location' => {
data_type => 'text',
},
'poe_capable_ports' => {
data_type => 'bigint',
},
'poe_powered_ports' => {
data_type => 'bigint',
},
'poe_disabled_ports' => {
data_type => 'bigint',
},
'poe_errored_ports' => {
data_type => 'bigint',
},
'poe_power_committed' => {
data_type => 'numeric',
},
'poe_power_delivering' => {
data_type => 'numeric',
},
);
1;

View File

@@ -0,0 +1,61 @@
package App::Netdisco::DB::Result::Virtual::DuplexMismatch;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('duplex_mismatch');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT dp.ip AS left_ip, d1.dns AS left_dns, dp.port AS left_port, dp.duplex AS left_duplex,
di.ip AS right_ip, d2.dns AS right_dns, dp.remote_port AS right_port, dp2.duplex AS right_duplex
FROM ( SELECT device_port.ip, device_port.remote_ip, device_port.port, device_port.duplex, device_port.remote_port
FROM device_port
WHERE
device_port.remote_port IS NOT NULL
AND device_port.up NOT ILIKE '%down%'
GROUP BY device_port.ip, device_port.remote_ip, device_port.port, device_port.duplex, device_port.remote_port
ORDER BY device_port.ip) dp
LEFT JOIN device_ip di ON dp.remote_ip = di.alias
LEFT JOIN device d1 ON dp.ip = d1.ip
LEFT JOIN device d2 ON di.ip = d2.ip
LEFT JOIN device_port dp2 ON (di.ip = dp2.ip AND dp.remote_port = dp2.port)
WHERE di.ip IS NOT NULL
AND dp.duplex <> dp2.duplex
AND dp.ip <= di.ip
AND dp2.up NOT ILIKE '%down%'
ORDER BY dp.ip
ENDSQL
);
__PACKAGE__->add_columns(
'left_ip' => {
data_type => 'inet',
},
'left_dns' => {
data_type => 'text',
},
'left_port' => {
data_type => 'text',
},
'left_duplex' => {
data_type => 'text',
},
'right_ip' => {
data_type => 'inet',
},
'right_dns' => {
data_type => 'text',
},
'right_port' => {
data_type => 'text',
},
'right_duplex' => {
data_type => 'text',
},
);
1;

View File

@@ -0,0 +1,13 @@
package App::Netdisco::DB::Result::Virtual::GenericReport;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("generic_report");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{});
1;

View File

@@ -0,0 +1,19 @@
use utf8;
package App::Netdisco::DB::Result::Virtual::NodeIp4;
use strict;
use warnings;
use base 'App::Netdisco::DB::Result::NodeIp';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("node_ip4");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{
SELECT * FROM node_ip WHERE family(ip) = 4
});
1;

View File

@@ -0,0 +1,19 @@
use utf8;
package App::Netdisco::DB::Result::Virtual::NodeIp6;
use strict;
use warnings;
use base 'App::Netdisco::DB::Result::NodeIp';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("node_ip6");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{
SELECT * FROM node_ip WHERE family(ip) = 6
});
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

@@ -0,0 +1,27 @@
use utf8;
package App::Netdisco::DB::Result::Virtual::NodeWithAge;
use strict;
use warnings;
use base 'App::Netdisco::DB::Result::Node';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("node_with_age");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(q{
SELECT *,
replace( date_trunc( 'minute', age( now(), time_last + interval '30 second' ) ) ::text, 'mon', 'month')
AS time_last_age
FROM node
});
__PACKAGE__->add_columns(
"time_last_age",
{ data_type => "text", is_nullable => 1 },
);
1;

View File

@@ -0,0 +1,68 @@
package App::Netdisco::DB::Result::Virtual::NodesDiscovered;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('nodes_discovered');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT d.ip,
d.dns,
d.name,
p.port,
p.remote_ip,
p.remote_port,
p.remote_type,
p.remote_id
FROM device_port p,
device d
WHERE d.ip = p.ip
AND NOT EXISTS
(SELECT 1
FROM device_port q
WHERE q.ip = p.remote_ip
AND q.port = p.remote_port)
AND NOT EXISTS
(SELECT 1
FROM device_ip a,
device_port q
WHERE a.alias = p.remote_ip
AND q.ip = a.ip
AND q.port = p.remote_port)
AND (p.remote_id IS NOT NULL OR p.remote_type IS NOT NULL)
ORDER BY d.name, p.port
ENDSQL
);
__PACKAGE__->add_columns(
'ip' => {
data_type => 'inet',
},
'dns' => {
data_type => 'text',
},
'name' => {
data_type => 'text',
},
'port' => {
data_type => 'text',
},
'remote_ip' => {
data_type => 'inet',
},
'remote_port' => {
data_type => 'text',
},
'remote_type' => {
data_type => 'text',
},
'remote_id' => {
data_type => 'text',
},
);
1;

View File

@@ -0,0 +1,32 @@
package App::Netdisco::DB::Result::Virtual::OrphanedDevices;
use strict;
use warnings;
use utf8;
use base 'App::Netdisco::DB::Result::Device';
__PACKAGE__->load_components('Helper::Row::SubClass');
__PACKAGE__->subclass;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('orphaned_devices');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT *
FROM device
WHERE ip NOT IN
( SELECT DISTINCT dp.ip AS ip
FROM
(SELECT device_port.ip,
device_port.remote_ip
FROM device_port
WHERE device_port.remote_port IS NOT NULL
GROUP BY device_port.ip,
device_port.remote_ip
ORDER BY device_port.ip) dp
LEFT JOIN device_ip di ON dp.remote_ip = di.alias
WHERE di.ip IS NOT NULL)
ENDSQL
1;

View File

@@ -0,0 +1,42 @@
package App::Netdisco::DB::Result::Virtual::PollerPerformance;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('poller_performance');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT action, entered, to_char( entered, 'YYYY-MM-DD HH24:MI:SS' ) AS entered_stamp,
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', 'nbtstat' )
GROUP BY action, entered
HAVING count( device ) > 1
ORDER BY entered DESC, elapsed DESC
LIMIT 30
ENDSQL
);
__PACKAGE__->add_columns(
"action",
{ data_type => "text", is_nullable => 1 },
"entered",
{ data_type => "timestamp", is_nullable => 1 },
"entered_stamp",
{ data_type => "text", is_nullable => 1 },
"number",
{ data_type => "integer", is_nullable => 1 },
"start",
{ data_type => "timestamp", is_nullable => 1 },
"end",
{ data_type => "timestamp", is_nullable => 1 },
"elapsed",
{ data_type => "interval", is_nullable => 1 },
);
1;

View File

@@ -0,0 +1,46 @@
package App::Netdisco::DB::Result::Virtual::PortUtilization;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('port_utilization');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT d.dns AS dns, d.ip as ip,
sum(CASE WHEN (dp.type != 'propVirtual') THEN 1 ELSE 0 END) as port_count,
sum(CASE WHEN (dp.type != 'propVirtual' AND dp.up_admin = 'up' AND dp.up = 'up') THEN 1 ELSE 0 END) as ports_in_use,
sum(CASE WHEN (dp.type != 'propVirtual' AND dp.up_admin != 'up') THEN 1 ELSE 0 END) as ports_shutdown,
sum(CASE WHEN (dp.type != 'propVirtual' AND dp.up_admin = 'up' AND dp.up != 'up') THEN 1 ELSE 0 END) as ports_free
FROM device d LEFT JOIN device_port dp
ON d.ip = dp.ip
GROUP BY d.dns, d.ip
ORDER BY d.dns, d.ip
ENDSQL
);
__PACKAGE__->add_columns(
'dns' => {
data_type => 'text',
},
'ip' => {
data_type => 'inet',
},
'port_count' => {
data_type => 'integer',
},
'ports_in_use' => {
data_type => 'integer',
},
'ports_shutdown' => {
data_type => 'integer',
},
'ports_free' => {
data_type => 'integer',
},
);
1;

View File

@@ -0,0 +1,42 @@
package App::Netdisco::DB::Result::Virtual::SlowDevices;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('slow_devices');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT a.action, a.device, a.started, a.finished,
justify_interval(extract(epoch FROM (a.finished - a.started)) * interval '1 second') AS elapsed
FROM admin a
INNER JOIN (
SELECT device, action, max(started) AS started
FROM admin
WHERE status = 'done'
AND action IN ('discover','macsuck','arpnip')
GROUP BY action, device
) b
ON a.device = b.device AND a.started = b.started
ORDER BY elapsed desc, action, device
LIMIT 20
ENDSQL
);
__PACKAGE__->add_columns(
"action",
{ data_type => "text", is_nullable => 1 },
"device",
{ data_type => "inet", is_nullable => 1 },
"started",
{ data_type => "timestamp", is_nullable => 1 },
"finished",
{ data_type => "timestamp", is_nullable => 1 },
"elapsed",
{ data_type => "interval", is_nullable => 1 },
);
1;

View File

@@ -0,0 +1,54 @@
package App::Netdisco::DB::Result::Virtual::SubnetUtilization;
use strict;
use warnings;
use utf8;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('cidr_ips');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT net as subnet,
power(2, (32 - masklen(net))) as subnet_size,
count(DISTINCT ip) as active,
round(100 * count(DISTINCT ip) / (power(2, (32 - masklen(net))))) as percent
FROM (
SELECT DISTINCT net, ni.ip
FROM subnets s1, node_ip ni
WHERE s1.net <<= ?::cidr
AND ni.ip <<= s1.net
AND ((
ni.time_first IS null
AND ni.time_last IS null
) OR (
ni.time_last >= ?
AND ni.time_last <= ?
))
AND s1.last_discover >= ?
UNION
SELECT DISTINCT net, di.alias as ip
FROM subnets s2, device_ip di JOIN device d USING (ip)
WHERE s2.net <<= ?::cidr
AND di.alias <<= s2.net
AND s2.last_discover >= ?
AND d.last_discover >= ?
) as joined
GROUP BY net
ORDER BY percent ASC
ENDSQL
__PACKAGE__->add_columns(
"subnet",
{ data_type => "cidr", is_nullable => 0 },
"subnet_size",
{ data_type => "integer", is_nullable => 0 },
"active",
{ data_type => "integer", is_nullable => 0 },
"percent",
{ data_type => "integer", is_nullable => 0 },
);
1;

View File

@@ -0,0 +1,54 @@
package App::Netdisco::DB::Result::Virtual::UnDirEdgesAgg;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('undir_edges_agg');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT left_ip,
array_agg(right_ip) AS links
FROM
( SELECT dp.ip AS left_ip,
di.ip AS right_ip
FROM
(SELECT device_port.ip,
device_port.remote_ip
FROM device_port
WHERE device_port.remote_port IS NOT NULL
GROUP BY device_port.ip,
device_port.remote_ip) dp
LEFT JOIN device_ip di ON dp.remote_ip = di.alias
WHERE di.ip IS NOT NULL
UNION SELECT di.ip AS left_ip,
dp.ip AS right_ip
FROM
(SELECT device_port.ip,
device_port.remote_ip
FROM device_port
WHERE device_port.remote_port IS NOT NULL
GROUP BY device_port.ip,
device_port.remote_ip) dp
LEFT JOIN device_ip di ON dp.remote_ip = di.alias
WHERE di.ip IS NOT NULL ) AS foo
GROUP BY left_ip
ORDER BY left_ip
ENDSQL
__PACKAGE__->add_columns(
'left_ip' => {
data_type => 'inet',
},
'links' => {
data_type => 'inet[]',
}
);
__PACKAGE__->belongs_to('device', 'App::Netdisco::DB::Result::Device',
{ 'foreign.ip' => 'self.left_ip' });
1;

View File

@@ -0,0 +1,61 @@
package App::Netdisco::DB::Result::Virtual::UndiscoveredNeighbors;
use strict;
use warnings;
use utf8;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('undiscovered_neighbors');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<'ENDSQL');
SELECT DISTINCT ON (p.remote_ip) d.ip,
d.name,
d.dns,
p.port,
p.remote_ip,
p.remote_id,
p.remote_type,
p.remote_port,
a.log,
a.finished
FROM device_port p
JOIN device d
ON d.ip = p.ip
LEFT JOIN admin a
ON (p.remote_ip = a.device AND a.action = 'discover')
WHERE
(p.remote_ip NOT IN (SELECT alias FROM device_ip))
OR
((p.remote_ip IS NULL) AND p.is_uplink)
ORDER BY
p.remote_ip ASC,
a.finished DESC
ENDSQL
__PACKAGE__->add_columns(
"ip",
{ data_type => "inet", is_nullable => 0 },
"name",
{ data_type => "text", is_nullable => 1 },
"dns",
{ data_type => "text", is_nullable => 1 },
"port",
{ data_type => "text", is_nullable => 0 },
"remote_ip",
{ data_type => "inet", is_nullable => 1 },
"remote_port",
{ data_type => "text", is_nullable => 1 },
"remote_type",
{ data_type => "text", is_nullable => 1 },
"remote_id",
{ data_type => "text", is_nullable => 1 },
"log",
{ data_type => "text", is_nullable => 1 },
"finished",
{ data_type => "timestamp", is_nullable => 1 },
);
1;

View File

@@ -0,0 +1,30 @@
use utf8;
package App::Netdisco::DB::Result::Virtual::UserRole;
use strict;
use warnings;
use base 'DBIx::Class::Core';
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table("user_role");
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(<<ENDSQL
SELECT username, 'port_control' AS role FROM users
WHERE port_control
UNION
SELECT username, 'admin' AS role FROM users
WHERE admin
UNION
SELECT username, 'ldap' AS role FROM users
WHERE ldap
ENDSQL
);
__PACKAGE__->add_columns(
'username' => { data_type => 'text' },
'role' => { data_type => 'text' },
);
1;

View File

@@ -0,0 +1,195 @@
package App::Netdisco::DB::ResultSet;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';
__PACKAGE__->load_components(
qw{Helper::ResultSet::SetOperations Helper::ResultSet::Shortcut});
=head1 ADDITIONAL METHODS
=head2 get_distinct_col( $column )
Returns an asciibetical sorted list of the distinct values in the given column
of the Device table. This is useful for web forms when you want to provide a
drop-down list of possible options.
=cut
sub get_distinct_col {
my ( $rs, $col ) = @_;
return $rs unless $col;
return $rs->search(
{},
{ columns => [$col],
order_by => $col,
distinct => 1
}
)->get_column($col)->all;
}
=head2 get_datatables_data( $params )
Returns a ResultSet for DataTables Server-side processing which populates
the displayed table. Evaluates the supplied query parameters for filtering,
paging, and ordering information. Note: query paramters are expected to be
passed as a reference to an expanded hash of hashes.
Filtering if present, will generate simple LIKE matching conditions for each
searchable column (searchability indicated by query parameters) after each
column is casted to text. Conditions are combined as disjunction (OR).
Note: this does not match the built-in DataTables filtering which does it
word by word on any field.
=cut
sub get_datatables_data {
my $rs = shift;
my $params = shift;
my $attrs = shift;
die "condition parameter to search_by_field must be hashref\n"
if ref {} ne ref $params
or 0 == scalar keys %$params;
# -- Paging
$rs = $rs->_with_datatables_paging($params);
# -- Ordering
$rs = $rs->_with_datatables_order_clause($params);
# -- Filtering
$rs = $rs->_with_datatables_where_clause($params);
return $rs;
}
=head2 get_datatables_filtered_count( $params )
Returns the total records, after filtering (i.e. the total number of
records after filtering has been applied - not just the number of records
being returned for this page of data) for a datatables ResultSet and
query parameters. Note: query paramters are expected to be passed as a
reference to an expanded hash of hashes.
=cut
sub get_datatables_filtered_count {
my $rs = shift;
my $params = shift;
return $rs->_with_datatables_where_clause($params)->count;
}
sub _with_datatables_order_clause {
my $rs = shift;
my $params = shift;
my $attrs = shift;
my @order = ();
if ( defined $params->{'order'}{0} ) {
for ( my $i = 0; $i < (scalar keys %{$params->{'order'}}); $i++ ) {
# build direction, must be '-asc' or '-desc' (cf. SQL::Abstract)
# we only get 'asc' or 'desc', so they have to be prefixed with '-'
my $direction = '-' . $params->{'order'}{$i}{'dir'};
# We only get the column index (starting from 0), so we have to
# translate the index into a column name.
my $column_name = _datatables_index_to_column( $params,
$params->{'order'}{$i}{'column'} );
# Prefix with table alias if no prefix
my $csa = $rs->current_source_alias;
$column_name =~ s/^(\w+)$/$csa\.$1/x;
push @order, { $direction => $column_name };
}
}
$rs = $rs->order_by( \@order );
return $rs;
}
# NOTE this does not match the built-in DataTables filtering which does it
# word by word on any field.
#
# General filtering using LIKE, this will not be efficient as is will not
# be able to use indexes.
sub _with_datatables_where_clause {
my $rs = shift;
my $params = shift;
my $attrs = shift;
my %where = ();
if ( defined $params->{'search'}{'value'}
&& $params->{'search'}{'value'} )
{
my $search_string = $params->{'search'}{'value'};
for ( my $i = 0; $i < (scalar keys %{$params->{'columns'}}); $i++ ) {
# Iterate over each column and check if it is searchable.
# If so, add a constraint to the where clause restricting the given
# column. In the query, the column is identified by it's index, we
# need to translate the index to the column name.
if ( $params->{'columns'}{$i}{'searchable'}
and $params->{'columns'}{$i}{'searchable'} eq 'true' )
{
my $column = _datatables_index_to_column( $params, $i );
my $csa = $rs->current_source_alias;
$column =~ s/^(\w+)$/$csa\.$1/x;
# Cast everything to text for LIKE search
$column = $column . '::text';
push @{ $where{'-or'} },
{ $column => { -like => '%' . $search_string . '%' } };
}
}
}
$rs = $rs->search( \%where, $attrs );
return $rs;
}
sub _with_datatables_paging {
my $rs = shift;
my $params = shift;
my $attrs = shift;
my $limit = $params->{'length'};
my $offset = 0;
if ( defined $params->{'start'} && $params->{'start'} ) {
$offset = $params->{'start'};
}
$attrs->{'offset'} = $offset;
$rs = $rs->search( {}, $attrs );
$rs = $rs->limit($limit) if ($limit and $limit > 0);
return $rs;
}
# Use the DataTables columns.data definition to derive the column
# name from the index.
sub _datatables_index_to_column {
my $params = shift;
my $i = shift;
my $field;
if ( !defined($i) ) {
$i = 0;
}
$field = $params->{'columns'}{$i}{'data'};
return $field;
}
1;

View File

@@ -0,0 +1,45 @@
package App::Netdisco::DB::ResultSet::Admin;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
=head1 ADDITIONAL METHODS
=head2 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 entered_stamp
=item started_stamp
=item finished_stamp
=back
=cut
sub with_times {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => {
entered_stamp => \"to_char(entered, 'YYYY-MM-DD HH24:MI')",
started_stamp => \"to_char(started, 'YYYY-MM-DD HH24:MI')",
finished_stamp => \"to_char(finished, 'YYYY-MM-DD HH24:MI')",
},
});
}
1;

View File

@@ -0,0 +1,612 @@
package App::Netdisco::DB::ResultSet::Device;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
use NetAddr::IP::Lite ':lower';
=head1 ADDITIONAL METHODS
=head2 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 uptime_age
=item last_discover_stamp
=item last_macsuck_stamp
=item last_arpnip_stamp
=back
=cut
sub with_times {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => {
uptime_age => \("replace(age(timestamp 'epoch' + uptime / 100 * interval '1 second', "
."timestamp '1970-01-01 00:00:00-00')::text, 'mon', 'month')"),
last_discover_stamp => \"to_char(last_discover, 'YYYY-MM-DD HH24:MI')",
last_macsuck_stamp => \"to_char(last_macsuck, 'YYYY-MM-DD HH24:MI')",
last_arpnip_stamp => \"to_char(last_arpnip, 'YYYY-MM-DD HH24:MI')",
since_last_discover => \"extract(epoch from (age(now(), last_discover)))",
since_last_macsuck => \"extract(epoch from (age(now(), last_macsuck)))",
since_last_arpnip => \"extract(epoch from (age(now(), last_arpnip)))",
},
});
}
=head2 search_aliases( {$name or $ip or $prefix}, \%options? )
Tries to find devices in Netdisco which have an identity corresponding to
C<$name>, C<$ip> or C<$prefix>.
The search is across all aliases of the device, as well as its "root IP"
identity. Note that this search will try B<not> to use DNS, in case the current
name for an IP does not correspond to the data within Netdisco.
Passing a zero value to the C<partial> key of the C<options> hashref will
prevent partial matching of a host name. Otherwise the default is to perform
a partial, case-insensitive search on the host name fields.
=cut
sub search_aliases {
my ($rs, $q, $options) = @_;
$q ||= '255.255.255.255'; # hack to return empty resultset on error
$options ||= {};
$options->{partial} = 1 if !defined $options->{partial};
# rough approximation of IP addresses (v4 in v6 not supported).
# this helps us avoid triggering any DNS.
my $by_ip = ($q =~ m{^(?:[.0-9/]+|[:0-9a-f/]+)$}i) ? 1 : 0;
my $clause;
if ($by_ip) {
my $ip = NetAddr::IP::Lite->new($q)
or return undef; # could be a MAC address!
$clause = [
'me.ip' => { '<<=' => $ip->cidr },
'device_ips.alias' => { '<<=' => $ip->cidr },
];
}
else {
$q = "\%$q\%" if ($options->{partial} and $q !~ m/\%/);
$clause = [
'me.name' => { '-ilike' => $q },
'me.dns' => { '-ilike' => $q },
'device_ips.dns' => { '-ilike' => $q },
];
}
return $rs->search(
{
-or => $clause,
},
{
order_by => [qw/ me.dns me.ip /],
join => 'device_ips',
distinct => 1,
}
);
}
=head2 search_for_device( $name or $ip or $prefix )
This is a wrapper for C<search_aliases> which:
=over 4
=item *
Disables partial matching on host names
=item *
Returns only the first result of any found devices
=back
If not matching devices are found, C<undef> is returned.
=cut
sub search_for_device {
my ($rs, $q, $options) = @_;
$options ||= {};
$options->{partial} = 0;
return $rs->search_aliases($q, $options)->first();
}
=head2 search_by_field( \%cond, \%attrs? )
This variant of the standard C<search()> method returns a ResultSet of Device
entries. It is written to support web forms which accept fields that match and
locate Devices in the database.
The hashref parameter should contain fields from the Device table which will
be intelligently used in a search query.
In addition, you can provide the key C<matchall> which, given a True or False
value, controls whether fields must all match or whether any can match, to
select a row.
Supported keys:
=over 4
=item matchall
If a True value, fields must all match to return a given row of the Device
table, otherwise any field matching will cause the row to be included in
results.
=item name
Can match the C<name> field as a substring.
=item location
Can match the C<location> field as a substring.
=item description
Can match the C<description> field as a substring (usually this field contains
a description of the vendor operating system).
=item model
Will match exactly the C<model> field.
=item os
Will match exactly the C<os> field, which is the operating sytem.
=item os_ver
Will match exactly the C<os_ver> field, which is the operating sytem software version.
=item vendor
Will match exactly the C<vendor> (manufacturer).
=item dns
Can match any of the Device IP address aliases as a substring.
=item ip
Can be a string IP or a NetAddr::IP object, either way being treated as an
IPv4 or IPv6 prefix within which the device must have one IP address alias.
=back
=cut
sub search_by_field {
my ($rs, $p, $attrs) = @_;
die "condition parameter to search_by_field must be hashref\n"
if ref {} ne ref $p or 0 == scalar keys %$p;
my $op = $p->{matchall} ? '-and' : '-or';
# this is a bit of an inelegant trick to catch junk data entry,
# whilst avoiding returning *all* entries in the table
if ($p->{ip} and 'NetAddr::IP::Lite' ne ref $p->{ip}) {
$p->{ip} = ( NetAddr::IP::Lite->new($p->{ip})
|| NetAddr::IP::Lite->new('255.255.255.255') );
}
# For Search on Layers
my @layer_search = ( '_', '_', '_', '_', '_', '_', '_' );
# @layer_search is computer indexed, left->right
my $layers = $p->{layers};
if ( defined $layers && ref $layers ) {
foreach my $layer (@$layers) {
next unless defined $layer and length($layer);
next if ( $layer < 1 || $layer > 7 );
$layer_search[ $layer - 1 ] = 1;
}
}
elsif ( defined $layers ) {
$layer_search[ $layers - 1 ] = 1;
}
# the database field is in order 87654321
my $layer_string = join( '', reverse @layer_search );
return $rs
->search_rs({}, $attrs)
->search({
$op => [
($p->{name} ? ('me.name' =>
{ '-ilike' => "\%$p->{name}\%" }) : ()),
($p->{location} ? ('me.location' =>
{ '-ilike' => "\%$p->{location}\%" }) : ()),
($p->{description} ? ('me.description' =>
{ '-ilike' => "\%$p->{description}\%" }) : ()),
($p->{layers} ? ('me.layers' =>
{ '-ilike' => "\%$layer_string" }) : ()),
($p->{model} ? ('me.model' =>
{ '-in' => $p->{model} }) : ()),
($p->{os} ? ('me.os' =>
{ '-in' => $p->{os} }) : ()),
($p->{os_ver} ? ('me.os_ver' =>
{ '-in' => $p->{os_ver} }) : ()),
($p->{vendor} ? ('me.vendor' =>
{ '-in' => $p->{vendor} }) : ()),
($p->{dns} ? (
-or => [
'me.dns' => { '-ilike' => "\%$p->{dns}\%" },
'device_ips.dns' => { '-ilike' => "\%$p->{dns}\%" },
]) : ()),
($p->{ip} ? (
-or => [
'me.ip' => { '<<=' => $p->{ip}->cidr },
'device_ips.alias' => { '<<=' => $p->{ip}->cidr },
]) : ()),
],
},
{
order_by => [qw/ me.dns me.ip /],
(($p->{dns} or $p->{ip}) ? (
join => 'device_ips',
distinct => 1,
) : ()),
}
);
}
=head2 search_fuzzy( $value )
This method accepts a single parameter only and returns a ResultSet of rows
from the Device table where one field matches the passed parameter.
The following fields are inspected for a match:
=over 4
=item contact
=item serial
=item location
=item name
=item description
=item dns
=item ip (including aliases)
=back
=cut
sub search_fuzzy {
my ($rs, $q) = @_;
die "missing param to search_fuzzy\n"
unless $q;
$q = "\%$q\%" if $q !~ m/\%/;
# basic IP check is a string match
my $ip_clause = [
'me.ip::text' => { '-ilike' => $q },
'device_ips.alias::text' => { '-ilike' => $q },
];
# but also allow prefix search
(my $qc = $q) =~ s/\%//g;
if (my $ip = NetAddr::IP::Lite->new($qc)) {
$ip_clause = [
'me.ip' => { '<<=' => $ip->cidr },
'device_ips.alias' => { '<<=' => $ip->cidr },
];
}
return $rs->search(
{
-or => [
'me.contact' => { '-ilike' => $q },
'me.serial' => { '-ilike' => $q },
'me.location' => { '-ilike' => $q },
'me.name' => { '-ilike' => $q },
'me.description' => { '-ilike' => $q },
-or => [
'me.dns' => { '-ilike' => $q },
'device_ips.dns' => { '-ilike' => $q },
],
-or => $ip_clause,
],
},
{
order_by => [qw/ me.dns me.ip /],
join => 'device_ips',
distinct => 1,
}
);
}
=head2 carrying_vlan( \%cond, \%attrs? )
my $set = $rs->carrying_vlan({ vlan => 123 });
Like C<search()>, this returns a ResultSet of matching rows from the Device
table.
The returned devices each are aware of the given Vlan.
=over 4
=item *
The C<cond> parameter must be a hashref containing a key C<vlan> with
the value to search for.
=item *
Results are ordered by the Device DNS and IP fields.
=item *
Related rows from the C<device_vlan> table will be prefetched.
=back
=cut
sub carrying_vlan {
my ($rs, $cond, $attrs) = @_;
die "vlan number required for carrying_vlan\n"
if ref {} ne ref $cond or !exists $cond->{vlan};
return $rs
->search_rs({ 'vlans.vlan' => $cond->{vlan} },
{
order_by => [qw/ me.dns me.ip /],
columns => [
'me.ip', 'me.dns',
'me.model', 'me.os',
'me.vendor', 'vlans.vlan',
'vlans.description'
],
join => 'vlans'
})
->search({}, $attrs);
}
=head2 carrying_vlan_name( \%cond, \%attrs? )
my $set = $rs->carrying_vlan_name({ name => 'Branch Office' });
Like C<search()>, this returns a ResultSet of matching rows from the Device
table.
The returned devices each are aware of the named Vlan.
=over 4
=item *
The C<cond> parameter must be a hashref containing a key C<name> with
the value to search for. The value may optionally include SQL wildcard
characters.
=item *
Results are ordered by the Device DNS and IP fields.
=item *
Related rows from the C<device_vlan> table will be prefetched.
=back
=cut
sub carrying_vlan_name {
my ($rs, $cond, $attrs) = @_;
die "vlan name required for carrying_vlan_name\n"
if ref {} ne ref $cond or !exists $cond->{name};
$cond->{'vlans.description'} = { '-ilike' => delete $cond->{name} };
return $rs
->search_rs({}, {
order_by => [qw/ me.dns me.ip /],
columns => [
'me.ip', 'me.dns',
'me.model', 'me.os',
'me.vendor', 'vlans.vlan',
'vlans.description'
],
join => 'vlans'
})
->search($cond, $attrs);
}
=head2 has_layer( $layer )
my $rset = $rs->has_layer(3);
This predefined C<search()> returns a ResultSet of matching rows from the
Device table of devices advertising support of the supplied layer in the
OSI Model.
=over 4
=item *
The C<layer> parameter must be an integer between 1 and 7.
=cut
sub has_layer {
my ( $rs, $layer ) = @_;
die "layer required and must be between 1 and 7\n"
if !$layer || $layer < 1 || $layer > 7;
return $rs->search_rs( \[ 'substring(layers,9-?, 1)::int = 1', $layer ] );
}
=back
=head2 get_models
Returns a sorted list of Device models with the following columns only:
=over 4
=item vendor
=item model
=item count
=back
Where C<count> is the number of instances of that Vendor's Model in the
Netdisco database.
=cut
sub get_models {
my $rs = shift;
return $rs->search({}, {
select => [ 'vendor', 'model', { count => 'ip' } ],
as => [qw/vendor model count/],
group_by => [qw/vendor model/],
order_by => [{-asc => 'vendor'}, {-asc => 'model'}],
})
}
=head2 get_releases
Returns a sorted list of Device OS releases with the following columns only:
=over 4
=item os
=item os_ver
=item count
=back
Where C<count> is the number of devices running that OS release in the
Netdisco database.
=cut
sub get_releases {
my $rs = shift;
return $rs->search({}, {
select => [ 'os', 'os_ver', { count => 'ip' } ],
as => [qw/os os_ver count/],
group_by => [qw/os os_ver/],
order_by => [{-asc => 'os'}, {-asc => 'os_ver'}],
})
}
=head2 with_port_count
This is a modifier for any C<search()> which
will add the following additional synthesized column to the result set:
=over 4
=item port_count
=back
=cut
sub with_port_count {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => {
port_count =>
$rs->result_source->schema->resultset('DevicePort')
->search(
{
'dp.ip' => { -ident => 'me.ip' },
'dp.type' => { '!=' => 'propVirtual' },
},
{ alias => 'dp' }
)->count_rs->as_query,
},
});
}
=head1 SPECIAL METHODS
=head2 delete( \%options? )
Overrides the built-in L<DBIx::Class> delete method to more efficiently
handle the removal or archiving of nodes.
=cut
sub delete {
my $self = shift;
my $schema = $self->result_source->schema;
my $devices = $self->search(undef, { columns => 'ip' });
foreach my $set (qw/
DeviceIp
DeviceVlan
DevicePower
DeviceModule
Community
/) {
$schema->resultset($set)->search(
{ ip => { '-in' => $devices->as_query } },
)->delete;
}
$schema->resultset('Admin')->search({
device => { '-in' => $devices->as_query },
})->delete;
$schema->resultset('Topology')->search({
-or => [
{ dev1 => { '-in' => $devices->as_query } },
{ dev2 => { '-in' => $devices->as_query } },
],
})->delete;
$schema->resultset('DevicePort')->search(
{ ip => { '-in' => $devices->as_query } },
)->delete(@_);
# now let DBIC do its thing
return $self->next::method();
}
1;

View File

@@ -0,0 +1,107 @@
package App::Netdisco::DB::ResultSet::DeviceModule;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
=head1 ADDITIONAL METHODS
=head2 search_by_field( \%cond, \%attrs? )
This variant of the standard C<search()> method returns a ResultSet of Device
Module entries. It is written to support web forms which accept fields that
match and locate Device Modules in the database.
The hashref parameter should contain fields from the Device Module table
which will be intelligently used in a search query.
In addition, you can provide the key C<matchall> which, given a True or False
value, controls whether fields must all match or whether any can match, to
select a row.
Supported keys:
=over 4
=item matchall
If a True value, fields must all match to return a given row of the Device
table, otherwise any field matching will cause the row to be included in
results.
=item description
Can match the C<description> field as a substring.
=item name
Can match the C<name> field as a substring.
=item type
Can match the C<type> field as a substring.
=item model
Can match the C<model> field as a substring.
=item serial
Can match the C<serial> field as a substring.
=item class
Will match exactly the C<class> field.
=item ips
List of Device IPs containing modules.
=back
=cut
sub search_by_field {
my ( $rs, $p, $attrs ) = @_;
die "condition parameter to search_by_field must be hashref\n"
if ref {} ne ref $p
or 0 == scalar keys %$p;
my $op = $p->{matchall} ? '-and' : '-or';
return $rs->search_rs( {}, $attrs )->search(
{ $op => [
( $p->{description}
? ( 'me.description' =>
{ '-ilike' => "\%$p->{description}\%" } )
: ()
),
( $p->{name}
? ( 'me.name' => { '-ilike' => "\%$p->{name}\%" } )
: ()
),
( $p->{type}
? ( 'me.type' => { '-ilike' => "\%$p->{type}\%" } )
: ()
),
( $p->{model}
? ( 'me.model' => { '-ilike' => "\%$p->{model}\%" } )
: ()
),
( $p->{serial}
? ( 'me.serial' => { '-ilike' => "\%$p->{serial}\%" } )
: ()
),
( $p->{class}
? ( 'me.class' => { '-in' => $p->{class} } )
: ()
),
( $p->{ips} ? ( 'me.ip' => { '-in' => $p->{ips} } ) : () ),
],
}
);
}
1;

View File

@@ -0,0 +1,174 @@
package App::Netdisco::DB::ResultSet::DevicePort;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
=head1 ADDITIONAL METHODS
=head2 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 lastchange_stamp
=back
=cut
sub with_times {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => { lastchange_stamp =>
\("to_char(device.last_discover - (device.uptime - me.lastchange) / 100 * interval '1 second', "
."'YYYY-MM-DD HH24:MI:SS')") },
join => 'device',
});
}
=head2 with_free_ports
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 is_free
=back
In the C<$cond> hash (the first parameter) pass in the C<age_num> which must
be an integer, and the C<age_unit> which must be a string of either C<days>,
C<weeks>, C<months> or C<years>.
=cut
sub with_is_free {
my ($rs, $cond, $attrs) = @_;
my $interval = (delete $cond->{age_num}) .' '. (delete $cond->{age_unit});
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => { is_free =>
\["me.up != 'up' and "
."age(now(), to_timestamp(extract(epoch from device.last_discover) "
."- (device.uptime - me.lastchange)/100)) "
."> ?::interval",
[{} => $interval]] },
join => 'device',
});
}
=head2 only_free_ports
This is a modifier for any C<search()> (including the helpers below) which
will restrict results based on whether the port is considered "free".
In the C<$cond> hash (the first parameter) pass in the C<age_num> which must
be an integer, and the C<age_unit> which must be a string of either C<days>,
C<weeks>, C<months> or C<years>.
=cut
sub only_free_ports {
my ($rs, $cond, $attrs) = @_;
my $interval = (delete $cond->{age_num}) .' '. (delete $cond->{age_unit});
return $rs
->search_rs($cond, $attrs)
->search(
{
'me.up' => { '!=' => 'up' },
},{
where =>
\["age(now(), to_timestamp(extract(epoch from device.last_discover) "
."- (device.uptime - me.lastchange)/100)) "
."> ?::interval",
[{} => $interval]],
join => 'device' },
);
}
=head2 with_vlan_count
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 vlan_count
=back
=cut
sub with_vlan_count {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => { vlan_count =>
$rs->result_source->schema->resultset('DevicePortVlan')
->search(
{
'dpv.ip' => { -ident => 'me.ip' },
'dpv.port' => { -ident => 'me.port' },
},
{ alias => 'dpv' }
)->count_rs->as_query
},
});
}
=head1 SPECIAL METHODS
=head2 delete( \%options? )
Overrides the built-in L<DBIx::Class> delete method to more efficiently
handle the removal or archiving of nodes.
=cut
sub delete {
my $self = shift;
my $schema = $self->result_source->schema;
my $ports = $self->search(undef, { columns => 'ip' });
foreach my $set (qw/
DevicePortPower
DevicePortVlan
DevicePortWireless
DevicePortSsid
/) {
$schema->resultset($set)->search(
{ ip => { '-in' => $ports->as_query }},
)->delete;
}
$schema->resultset('Node')->search(
{ switch => { '-in' => $ports->as_query }},
)->delete(@_);
# now let DBIC do its thing
return $self->next::method();
}
1;

View File

@@ -0,0 +1,39 @@
package App::Netdisco::DB::ResultSet::DevicePortLog;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
=head1 ADDITIONAL METHODS
=head2 with_times
This is a modifier for any C<search()> which will add the following additional
synthesized column to the result set:
=over 4
=item creation_stamp
=back
=cut
sub with_times {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'+columns' => {
creation_stamp => \"to_char(creation, 'YYYY-MM-DD HH24:MI:SS')",
},
});
}
1;

View File

@@ -0,0 +1,48 @@
package App::Netdisco::DB::ResultSet::DevicePortSsid;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(
qw/
+App::Netdisco::DB::ExplicitLocking
/
);
=head1 ADDITIONAL METHODS
=head2 get_ssids
Returns a sorted list of SSIDs with the following columns only:
=over 4
=item ssid
=item broadcast
=item count
=back
Where C<count> is the number of instances of the SSID in the Netdisco
database.
=cut
sub get_ssids {
my $rs = shift;
return $rs->search(
{},
{ select => [ 'ssid', 'broadcast', { count => 'ssid' } ],
as => [qw/ ssid broadcast count /],
group_by => [qw/ ssid broadcast /],
order_by => { -desc => [qw/count/] },
}
)
}
1;

View File

@@ -0,0 +1,78 @@
package App::Netdisco::DB::ResultSet::DevicePower;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
=head1 ADDITIONAL METHODS
=head2 with_poestats
This is a modifier for any C<search()> which will add the following
additional synthesized columns to the result set:
=over 4
=item poe_capable_ports
Count of ports which have the ability to supply PoE.
=item poe_powered_ports
Count of ports with PoE administratively disabled.
=item poe_disabled_ports
Count of ports which are delivering power.
=item poe_errored_ports
Count of ports either reporting a fault or in test mode.
=item poe_power_committed
Total power that has been negotiated and therefore committed on ports
actively supplying power.
=item poe_power_delivering
Total power as measured on ports actively supplying power.
=back
=cut
sub with_poestats {
my ($rs, $cond, $attrs) = @_;
return $rs
->search_rs($cond, $attrs)
->search({},
{
'columns' => {
ip => \"DISTINCT ON (me.ip, me.module) me.ip",
module => 'module',
power => 'power::bigint',
status => 'status',
poe_capable_ports => \"COUNT(ports.port) OVER (PARTITION BY me.ip, me.module)",
poe_powered_ports => \"SUM(CASE WHEN ports.status = 'deliveringPower' THEN 1 ELSE 0 END) OVER (PARTITION BY me.ip, me.module)",
poe_disabled_ports => \"SUM(CASE WHEN ports.admin = 'false' THEN 1 ELSE 0 END) OVER (PARTITION BY me.ip, me.module)",
poe_errored_ports => \"SUM(CASE WHEN ports.status ILIKE '%fault' THEN 1 ELSE 0 END) OVER (PARTITION BY me.ip, me.module)",
poe_power_committed => \("SUM(CASE "
. "WHEN ports.status = 'deliveringPower' AND ports.class = 'class0' THEN 15.4 "
. "WHEN ports.status = 'deliveringPower' AND ports.class = 'class1' THEN 4.0 "
. "WHEN ports.status = 'deliveringPower' AND ports.class = 'class2' THEN 7.0 "
. "WHEN ports.status = 'deliveringPower' AND ports.class = 'class3' THEN 15.4 "
. "WHEN ports.status = 'deliveringPower' AND ports.class = 'class4' THEN 30.0 "
. "WHEN ports.status = 'deliveringPower' AND ports.class IS NULL THEN 15.4 "
. "ELSE 0 END) OVER (PARTITION BY me.ip, me.module)"),
poe_power_delivering => \("SUM(CASE WHEN (ports.power IS NULL OR ports.power = '0') "
. "THEN 0 ELSE round(ports.power/1000.0, 1) END) "
. "OVER (PARTITION BY me.ip, me.module)")
},
join => 'ports'
});
}
1;

View File

@@ -0,0 +1,149 @@
package App::Netdisco::DB::ResultSet::Node;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
=head1 ADDITIONAL METHODS
=head2 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 Node
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 Device table and the Device C<dns> column
prefetched.
=back
To limit results only to active nodes, 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};
$cond->{'me.mac'} = delete $cond->{mac};
return $rs
->search_rs({}, {
order_by => {'-desc' => 'time_last'},
'+columns' => [
'device.dns',
{ 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 => 'device',
})
->search($cond, $attrs);
}
=head1 SPECIAL METHODS
=head2 delete( \%options? )
Overrides the built-in L<DBIx::Class> delete method to more efficiently
handle the removal or archiving of nodes.
=cut
sub delete {
my $self = shift;
my ($opts) = @_;
$opts = {} if (ref {} ne ref $opts);
my $schema = $self->result_source->schema;
my $nodes = $self->search(undef, { columns => 'mac' });
if (exists $opts->{archive_nodes} and $opts->{archive_nodes}) {
foreach my $set (qw/
NodeIp
NodeNbt
NodeMonitor
Node
/) {
$schema->resultset($set)->search(
{ mac => { '-in' => $nodes->as_query }},
)->update({ active => \'false' });
}
$schema->resultset('NodeWireless')
->search({ mac => { '-in' => $nodes->as_query }})->delete;
# avoid letting DBIC delete nodes
return 0E0;
}
elsif (exists $opts->{only_nodes} and $opts->{only_nodes}) {
# now let DBIC do its thing
return $self->next::method();
}
elsif (exists $opts->{keep_nodes} and $opts->{keep_nodes}) {
# avoid letting DBIC delete nodes
return 0E0;
}
else {
# for node_ip and node_nbt *only* delete if there are no longer
# any active nodes referencing the IP or NBT (hence 2nd IN clause).
foreach my $set (qw/
NodeIp
NodeNbt
/) {
$schema->resultset($set)->search({
'me.mac' => { '-in' => $schema->resultset($set)->search({
'-and' => [
-bool => 'nodes.active',
'me.mac' => { '-in' => $nodes->as_query }
]
},
{
columns => 'mac',
join => 'nodes',
group_by => 'me.mac',
having => \[ 'count(nodes.mac) = 0' ],
})->as_query,
},
})->delete;
}
foreach my $set (qw/
NodeMonitor
NodeWireless
/) {
$schema->resultset($set)->search(
{ mac => { '-in' => $nodes->as_query }},
)->delete;
}
# now let DBIC do its thing
return $self->next::method();
}
}
1;

View File

@@ -0,0 +1,219 @@
package App::Netdisco::DB::ResultSet::NodeIp;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
my $search_attr = {
order_by => {'-desc' => 'time_last'},
'+columns' => [
'oui.company',
'oui.abbrev',
{ 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
NodeIp 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({dns => 'foo.example.com', active => 1});
Like C<search()>, this returns a ResultSet of matching rows from the
NodeIp table.
=over 4
=item *
The NodeIp table must have a C<dns> column for this search to work. Typically
this column is the IP's DNS PTR record, cached at the time of Netdisco Arpnip.
=item *
The C<cond> parameter must be a hashref containing a key C<dns> 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_dns {
my ($rs, $cond, $attrs) = @_;
die "dns field required for search_by_dns\n"
if ref {} ne ref $cond or !exists $cond->{dns};
$cond->{dns} = { '-ilike' => delete $cond->{dns} };
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
NodeIp 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);
}
=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(me.ip) = ?', $version ] );
}
1;

View File

@@ -0,0 +1,189 @@
package App::Netdisco::DB::ResultSet::NodeNbt;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__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

@@ -0,0 +1,11 @@
package App::Netdisco::DB::ResultSet::NodeWireless;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
1;

View File

@@ -0,0 +1,11 @@
package App::Netdisco::DB::ResultSet::Subnet;
use base 'App::Netdisco::DB::ResultSet';
use strict;
use warnings;
__PACKAGE__->load_components(qw/
+App::Netdisco::DB::ExplicitLocking
/);
1;

View File

@@ -0,0 +1,372 @@
BEGIN;
-- admin table - Queue for admin tasks sent from front-end for back-end processing.
CREATE TABLE admin (
job serial,
entered TIMESTAMP DEFAULT now(),
started TIMESTAMP,
finished TIMESTAMP,
device inet,
port text,
action text,
subaction text,
status text,
username text,
userip inet,
log text,
debug boolean
);
CREATE INDEX idx_admin_entered ON admin(entered);
CREATE INDEX idx_admin_status ON admin(status);
CREATE INDEX idx_admin_action ON admin(action);
CREATE TABLE device (
ip inet PRIMARY KEY,
creation TIMESTAMP DEFAULT now(),
dns text,
description text,
uptime bigint,
contact text,
name text,
location text,
layers varchar(8),
ports integer,
mac macaddr,
serial text,
model text,
ps1_type text,
ps2_type text,
ps1_status text,
ps2_status text,
fan text,
slots integer,
vendor text,
os text,
os_ver text,
log text,
snmp_ver integer,
snmp_comm text,
snmp_class text,
vtp_domain text,
last_discover TIMESTAMP,
last_macsuck TIMESTAMP,
last_arpnip TIMESTAMP
);
-- Indexing for speed-ups
CREATE INDEX idx_device_dns ON device(dns);
CREATE INDEX idx_device_layers ON device(layers);
CREATE INDEX idx_device_vendor ON device(vendor);
CREATE INDEX idx_device_model ON device(model);
CREATE TABLE device_ip (
ip inet,
alias inet,
subnet cidr,
port text,
dns text,
creation TIMESTAMP DEFAULT now(),
PRIMARY KEY(ip,alias)
);
-- Indexing for speed ups
CREATE INDEX idx_device_ip_ip ON device_ip(ip);
CREATE INDEX idx_device_ip_alias ON device_ip(alias);
CREATE INDEX idx_device_ip_ip_port ON device_ip(ip,port);
CREATE TABLE device_module (
ip inet not null,
index integer,
description text,
type text,
parent integer,
name text,
class text,
pos integer,
hw_ver text,
fw_ver text,
sw_ver text,
serial text,
model text,
fru boolean,
creation TIMESTAMP DEFAULT now(),
last_discover TIMESTAMP,
PRIMARY KEY(ip,index)
);
CREATE TABLE device_port (
ip inet,
port text,
creation TIMESTAMP DEFAULT now(),
descr text,
up text,
up_admin text,
type text,
duplex text,
duplex_admin text,
speed text,
name text,
mac macaddr,
mtu integer,
stp text,
remote_ip inet,
remote_port text,
remote_type text,
remote_id text,
vlan text,
pvid integer,
lastchange bigint,
PRIMARY KEY(port,ip)
);
CREATE INDEX idx_device_port_ip ON device_port(ip);
CREATE INDEX idx_device_port_remote_ip ON device_port(remote_ip);
-- For the duplex mismatch finder :
CREATE INDEX idx_device_port_ip_port_duplex ON device_port(ip,port,duplex);
CREATE INDEX idx_device_port_ip_up_admin ON device_port(ip,up_admin);
CREATE INDEX idx_device_port_mac ON device_port(mac);
CREATE TABLE device_port_log (
id serial,
ip inet,
port text,
reason text,
log text,
username text,
userip inet,
action text,
creation TIMESTAMP DEFAULT now()
);
CREATE INDEX idx_device_port_log_1 ON device_port_log(ip,port);
CREATE INDEX idx_device_port_log_user ON device_port_log(username);
CREATE TABLE device_port_power (
ip inet,
port text,
module integer,
admin text,
status text,
class text,
power integer,
PRIMARY KEY(port,ip)
);
CREATE TABLE device_port_ssid (
ip inet,
port text,
ssid text,
broadcast boolean,
bssid macaddr
);
CREATE INDEX idx_device_port_ssid_ip_port ON device_port_ssid(ip,port);
CREATE TABLE device_port_vlan (
ip inet,
port text,
vlan integer,
native boolean not null default false,
creation TIMESTAMP DEFAULT now(),
last_discover TIMESTAMP DEFAULT now(),
vlantype text,
PRIMARY KEY(ip,port,vlan)
);
CREATE TABLE device_port_wireless (
ip inet,
port text,
channel integer,
power integer
);
CREATE INDEX idx_device_port_wireless_ip_port ON device_port_wireless(ip,port);
CREATE TABLE device_power (
ip inet,
module integer,
power integer,
status text,
PRIMARY KEY(ip,module)
);
CREATE TABLE device_vlan (
ip inet,
vlan integer,
description text,
creation TIMESTAMP DEFAULT now(),
last_discover TIMESTAMP DEFAULT now(),
PRIMARY KEY(ip,vlan)
);
CREATE TABLE log (
id serial,
creation TIMESTAMP DEFAULT now(),
class text,
entry text,
logfile text
);
CREATE TABLE node (
mac macaddr,
switch inet,
port text,
vlan text default '0',
active boolean,
oui varchar(8),
time_first timestamp default now(),
time_recent timestamp default now(),
time_last timestamp default now(),
PRIMARY KEY(mac,switch,port,vlan)
);
-- Indexes speed things up a LOT
CREATE INDEX idx_node_switch_port_active ON node(switch,port,active);
CREATE INDEX idx_node_switch_port ON node(switch,port);
CREATE INDEX idx_node_switch ON node(switch);
CREATE INDEX idx_node_mac ON node(mac);
CREATE INDEX idx_node_mac_active ON node(mac,active);
-- CREATE INDEX idx_node_oui ON node(oui);
CREATE TABLE node_ip (
mac macaddr,
ip inet,
active boolean,
time_first timestamp default now(),
time_last timestamp default now(),
PRIMARY KEY(mac,ip)
);
-- Indexing speed ups.
CREATE INDEX idx_node_ip_ip ON node_ip(ip);
CREATE INDEX idx_node_ip_ip_active ON node_ip(ip,active);
CREATE INDEX idx_node_ip_mac ON node_ip(mac);
CREATE INDEX idx_node_ip_mac_active ON node_ip(mac,active);
CREATE TABLE node_monitor (
mac macaddr,
active boolean,
why text,
cc text,
date TIMESTAMP DEFAULT now(),
PRIMARY KEY(mac)
);
-- node_nbt - Hold Netbios information for each node.
CREATE TABLE node_nbt (
mac macaddr PRIMARY KEY,
ip inet,
nbname text,
domain text,
server boolean,
nbuser text,
active boolean,
time_first timestamp default now(),
time_last timestamp default now()
);
-- Indexing speed ups.
CREATE INDEX idx_node_nbt_mac ON node_nbt(mac);
CREATE INDEX idx_node_nbt_nbname ON node_nbt(nbname);
CREATE INDEX idx_node_nbt_domain ON node_nbt(domain);
CREATE INDEX idx_node_nbt_mac_active ON node_nbt(mac,active);
-- Add "vlan" column to node table
-- ALTER TABLE node ADD COLUMN vlan text default '0';
alter table node drop constraint node_pkey;
alter table node add primary key (mac, switch, port, vlan);
CREATE TABLE node_wireless (
mac macaddr,
ssid text default '',
uptime integer,
maxrate integer,
txrate integer,
sigstrength integer,
sigqual integer,
rxpkt integer,
txpkt integer,
rxbyte bigint,
txbyte bigint,
time_last timestamp default now(),
PRIMARY KEY(mac,ssid)
);
-- Add "ssid" column to node_wireless table
-- ALTER TABLE node_wireless ADD ssid text default '';
alter table node_wireless drop constraint node_wireless_pkey;
alter table node_wireless add primary key (mac, ssid);
CREATE TABLE oui (
oui varchar(8) PRIMARY KEY,
company text
);
-- process table - Queue to coordinate between processes in multi-process mode.
CREATE TABLE process (
controller integer not null,
device inet not null,
action text not null,
status text,
count integer,
creation TIMESTAMP DEFAULT now()
);
CREATE TABLE sessions (
id char(32) NOT NULL PRIMARY KEY,
creation TIMESTAMP DEFAULT now(),
a_session text
);
CREATE TABLE subnets (
net cidr NOT NULL,
creation timestamp default now(),
last_discover timestamp default now(),
PRIMARY KEY(net)
);
-- Add "topology" table to augment manual topo file
CREATE TABLE topology (
dev1 inet not null,
port1 text not null,
dev2 inet not null,
port2 text not null
);
-- This table logs login and logout / change requests for users
CREATE TABLE user_log (
entry serial,
username varchar(50),
userip inet,
event text,
details text,
creation TIMESTAMP DEFAULT now()
);
CREATE TABLE users (
username varchar(50) PRIMARY KEY,
password text,
creation TIMESTAMP DEFAULT now(),
last_on TIMESTAMP,
port_control boolean DEFAULT false,
ldap boolean DEFAULT false,
admin boolean DEFAULT false,
fullname text,
note text
);
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE device_port_vlan ADD COLUMN vlantype text;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node_ip ADD COLUMN dns text;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node DROP CONSTRAINT node_pkey;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node_wireless DROP CONSTRAINT node_wireless_pkey;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node ADD COLUMN vlan text DEFAULT '0' NOT NULL;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node_wireless ADD COLUMN ssid text DEFAULT '' NOT NULL;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
CREATE INDEX device_port_power_idx_ip_port on device_port_power (ip, port);
COMMIT;

View File

@@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE admin DROP CONSTRAINT IF EXISTS admin_pkey;
ALTER TABLE admin ADD PRIMARY KEY (job);
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node ADD PRIMARY KEY (mac, switch, port, vlan);
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE node_wireless ADD PRIMARY KEY (mac, ssid);
COMMIT;

View File

@@ -0,0 +1,14 @@
BEGIN;
-- Database Schema Modifications for upgrading from 0.9x to 0.93
ALTER TABLE device_port ADD COLUMN remote_type text;
ALTER TABLE device_port ADD COLUMN remote_id text;
ALTER TABLE device_port ADD COLUMN vlan text;
ALTER TABLE device ADD COLUMN vtp_domain text;
ALTER TABLE users ADD COLUMN fullname text;
ALTER TABLE users ADD COLUMN note text;
COMMIT;

View File

@@ -0,0 +1,10 @@
BEGIN;
CREATE UNIQUE INDEX jobs_queued ON admin (
action,
coalesce(subaction, '_x_'),
coalesce(device, '255.255.255.255'),
coalesce(port, '_x_')
) WHERE status LIKE 'queued%';
COMMIT;

View File

@@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE topology ADD CONSTRAINT topology_dev1_port1 UNIQUE (dev1, port1);
ALTER TABLE topology ADD CONSTRAINT topology_dev2_port2 UNIQUE (dev2, port2);
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE device_port ADD COLUMN "manual_topo" bool DEFAULT false NOT NULL;
COMMIT;

View File

@@ -0,0 +1,6 @@
BEGIN;
ALTER TABLE device_port ADD COLUMN "is_uplink" bool;
ALTER TABLE device_port ADD COLUMN "is_uplink_admin" bool;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE device_ip DROP CONSTRAINT "device_ip_alias";
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
-- ALTER TABLE device_port ALTER COLUMN remote_id TYPE bytea USING remote_id::bytea;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
-- ALTER TABLE device_port ALTER COLUMN remote_id TYPE text USING remote_id::text;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE device ADD COLUMN snmp_comm_rw text;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE device DROP COLUMN snmp_comm_rw;
COMMIT;

View File

@@ -0,0 +1,9 @@
BEGIN;
CREATE TABLE "community" (
"ip" inet NOT NULL,
"snmp_comm_rw" text,
PRIMARY KEY ("ip")
);
COMMIT;

View File

@@ -0,0 +1,38 @@
BEGIN;
-- Netdisco
-- Database Schema Modifications
-- UPGRADE from 0.93 to 0.94
ALTER TABLE device_port ADD COLUMN lastchange bigint;
ALTER TABLE log ADD COLUMN logfile text;
CREATE TABLE user_log (
entry serial,
username varchar(50),
userip inet,
event text,
details text,
creation TIMESTAMP DEFAULT now()
);
CREATE TABLE node_nbt (
mac macaddr PRIMARY KEY,
ip inet,
nbname text,
domain text,
server boolean,
nbuser text,
active boolean,
time_first timestamp default now(),
time_last timestamp default now()
);
-- Indexing speed ups.
CREATE INDEX idx_node_nbt_mac ON node_nbt(mac);
CREATE INDEX idx_node_nbt_nbname ON node_nbt(nbname);
CREATE INDEX idx_node_nbt_domain ON node_nbt(domain);
CREATE INDEX idx_node_nbt_mac_active ON node_nbt(mac,active);
COMMIT;

View File

@@ -0,0 +1,8 @@
BEGIN;
ALTER TABLE node_wireless ALTER COLUMN rxpkt TYPE bigint;
ALTER TABLE node_wireless ALTER COLUMN txpkt TYPE bigint;
ALTER TABLE node_wireless ALTER COLUMN rxbyte TYPE bigint;
ALTER TABLE node_wireless ALTER COLUMN txbyte TYPE bigint;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
DROP INDEX IF EXISTS jobs_queued;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
ALTER TABLE community ADD COLUMN snmp_auth_tag text;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
UPDATE node SET vlan = '0' WHERE vlan IS NULL;
COMMIT;

View File

@@ -0,0 +1,5 @@
BEGIN;
CREATE INDEX node_ip_idx_ip_active ON node_ip (ip, active);
COMMIT;

View File

@@ -0,0 +1,7 @@
BEGIN;
ALTER TABLE device_port_vlan DROP CONSTRAINT device_port_vlan_pkey;
ALTER TABLE device_port_vlan ADD PRIMARY KEY (ip, port, vlan, native);
COMMIT;

Some files were not shown because too many files have changed in this diff Show More