App-Netsync
view release on metacpan or search on metacpan
lib/App/Netsync.pm view on Meta::CPAN
package App::Netsync;
=head1 NAME
App::Netsync - network/database synchronization library
=head1 DESCRIPTION
This package can discover a network and synchronize it with a database.
=head1 SYNOPSIS
use App::Netsync;
App::Netsync::configure({
'Table' => 'assets',
'DeviceField' => 'SERIAL_NUMBER',
'InterfaceField' => 'PORT',
'InfoFields' => ['BLDG','ROOM','JACK'],
},{
'domain' => 'example.com',
},{
'Version' => 2,
'Community' => 'example',
},{
'DBMS' => 'pg',
'Server' => 'pg.example.com',
'Port' => 5432,
'Database' => 'example_db',
'Username' => 'user',
'Password' => 'pass',
});
my $nodes = App::Netsync::discover ('DNS','host[0-9]+');
App::Netsync::identify ($nodes,'DB',1);
App::Netsync::update ($nodes);
=cut
use 5.006;
use strict;
use warnings FATAL => 'all';
use autodie; #XXX Is autodie adequate?
use feature 'say';
use DBI;
use File::Basename;
use Net::DNS;
use Text::CSV;
use version;
use App::Netsync::Network;
use App::Netsync::Scribe 'note';
use App::Netsync::SNMP;
our ($SCRIPT,$VERSION);
our %config;
BEGIN {
($SCRIPT) = fileparse ($0,"\.[^.]*");
($VERSION) = version->declare('v4.0.0');
require Exporter;
our @ISA = ('Exporter');
our @EXPORT_OK = ('device_interfaces');
}
lib/App/Netsync.pm view on Meta::CPAN
configure the operating environment
B<Arguments>
I<( \%Netsync [, \%DNS [, \%SNMP [, \%DB ] ] ] )>
=over 3
=item Netsync
key-value pairs of Netsync environment settings
B<Available Environment Settings>
I<Note: If a default is not specified, the setting is required.>
=over 4
=item ConflictLog
where to log conflicts
default: F</var/log/E<lt>script nameE<gt>/conflicts.log>
=item DeviceField
the table field to use as a unique ID for devices
=item DeviceOrder
the width of fields specifying node and device counts
default: 4
I<Example>
=over 5
=item DeviceOrder = 3 (i.e. nodes < 1000), 500 nodes
> discovering (using DNS)... 500 nodes (50 skipped), 600 devices (50 stacks)
=item DeviceOrder = 9 (i.e. nodes < 1000000000), 500 nodes
> discovering (using DNS)... 500 nodes (50 skipped), 600 devices (50 stacks)
=item DeviceOrder = 1 (i.e. nodes < 10), 20 nodes !
> discovering (using DNS)... 111111111120 nodes (2 skipped), 24 devices (2 stacks)
=back
=item Indent
the number of spaces to use when output is indented
default: 4
=item InfoFields
which table fields to synchronize with device interfaces
=item InterfaceField
which table field to use as a unique ID for device interfaces
=item NodeLog
where to log all probed nodes
default: F</var/log/E<lt>script nameE<gt>/nodes.log>
=item Quiet
Print nothing.
default: 0
=item SyncOID
which OID to send synchronized information to when updating
default: ifAlias
=item Table
which database table to use
=item UpdateLog
where to log all modifications made to the network
default: F</var/log/E<lt>script nameE<gt>/updates.log>
=item Verbose
Print everything.
Note: Quiet mode overrides Verbose mode.
default: 0
=back
I<Netsync requires the following settings to use a DBMS:>
=over 4
=item DBMS
the database platform to use
=item Server
the server containing the database to use
=item Port
the port to contact the database server on
=item Database
the database to connect to
=item Username
the user to connect to the database as
=item Password
the authentication key to use to connect to the database
=back
I<Netsync requires the following settings to use SNMP:>
=over 4
=item MIBdir
the location of MIBs required by Netsync
lib/App/Netsync.pm view on Meta::CPAN
'default' => sub { @zone = split("\n",$node_source); },
);
($inputs{$node_source} || $inputs{'default'})->();
}
my ($skip_count,$device_count,$stack_count) = (0,0,0);
foreach (@zone) {
if (/^(?<host>$host_pattern)(\.(?:\S+\.)+\s+(?:\d+)\s+(?:\S+)\s+(?:A|AAAA))?\s+(?<ip>.+)$/) { #XXX Upgrade this to support a list of IP addresses (It currently supports dig output).
$nodes->{$+{'ip'}}{'ip'} = $+{'ip'};
my $node = $nodes->{$+{'ip'}};
$node->{'hostname'} = $+{'host'};
$node->{'RFC1035'} = $_;
# Gather information about the node.
my $serial_count = recognize $node;
if ($serial_count < 1) { # Otherwise, skip it.
++$skip_count;
delete $nodes->{$+{'ip'}};
}
else {
$device_count += $serial_count;
++$stack_count if $serial_count > 1;
# Show the user how many nodes have been discovered if necessary.
unless ($config{'Quiet'} or $config{'Verbose'}) {
print "\b"x$config{'DeviceOrder'};
printf ('%'.$config{'DeviceOrder'}.'d',scalar keys %$nodes);
}
}
}
}
# Show the user what's been found if necessary.
unless ($config{'Quiet'}) {
my $node_count = scalar keys %$nodes;
print $node_count if $config{'Verbose'};
print ' node';
print 's' if $node_count != 1;
print ' ('.$skip_count.' skipped)' if $skip_count > 0;
print ', '.$device_count.' device';
print 's' if $device_count != 1;
if ($stack_count > 0) {
print ' ('.$stack_count.' stack';
print 's' if $stack_count != 1;
print ')';
}
print "\n";
}
return $nodes;
}
################################################################################
sub synchronize { # Use information in the databse to update discovered nodes.
warn 'too few arguments' if @_ < 4;
warn 'too many arguments' if @_ > 4;
my ($nodes,$identified,$auto_match,$rows) = @_;
my $conflict_count = 0;
foreach my $row (@$rows) {
my $serial = uc $row->{$config{'DeviceField'}};
my $ifName = $row->{$config{'InterfaceField'}};
# Identify the device indicated by the given database entry.
my $node = $identified->{$serial};
unless (defined $node) {
my $device = device_find ($nodes,$serial);
next unless defined $device; # Otherwise, skip to the next entry.
$identified->{$serial} = $node = $device->{'node'};
}
# Identify the interface indicated by the given database entry.
my $device = $node->{'devices'}{$serial};
if ($auto_match and not defined $device->{'interfaces'}{$ifName}) {
foreach (sort keys %{$device->{'interfaces'}}) {
if (/[^0-9]$ifName$/) {
$ifName = $row->{$config{'InterfaceField'}} = $_;
last;
}
}
}
{ # Detect conflicts.
my $new_conflict_count = 0;
if (defined $device->{'interfaces'}{$ifName}) {
my $interface = $device->{'interfaces'}{$ifName};
if ($interface->{'identified'}) { # The database has a duplicate interface (only the first is used).
++$new_conflict_count;
note ($config{'ConflictLog'},interface_string ($interface).' duplicate');
}
else {
$interface->{'identified'} = 1;
foreach my $field (@{$config{'InfoFields'}}) { # Grab data to be pushed to the device.
$interface->{'info'}{$field} = $row->{$field};
}
# Show the user what's been found if necessary.
interface_dump $interface if $config{'Verbose'};
}
}
else { # An interface in the database could not be found on the indicated device.
++$new_conflict_count;
note ($config{'ConflictLog'},device_string ($device).' '.$ifName.' mismatch');
}
$conflict_count += $new_conflict_count;
}
}
return $conflict_count;
}
=head2 identify
identify discovered nodes in a database
lib/App/Netsync.pm view on Meta::CPAN
warn 'A database has not been configured.';
return undef;
}
# Connect to the database.
my $DSN = 'dbi:'.$config{'DBMS'};
$DSN .= ':host='.$config{'Server'};
$DSN .= ';port='.$config{'Port'};
$DSN .= ';database='.$config{'Database'};
my $db = DBI->connect($DSN,$config{'Username'},$config{'Password'},$config{'DB'});
my $query = $db->prepare('SELECT '.$fields.' FROM '.$config{'Table'});
$query->execute;
@data = @{$query->fetchall_arrayref({})};
$db->disconnect;
},
'default' => sub { # Read a CSV file specified with the database option (-d).
open (my $db,'<',$data_source);
my $parser = Text::CSV->new;
chomp (my @fields = split (',',<$db>));
$parser->column_names(@fields);
# Filter out fields that are not needed,
# and verify the presence of necessary fields.
my $removed_field_count = 0;
foreach my $i (keys @fields) {
$i -= $removed_field_count;
unless ($fields =~ /(^|,)$fields[$i](,|$)/) {
++$removed_field_count;
splice (@fields,$i,1);
}
}
die 'incompatible database' unless @fields == scalar split (',',$fields);
foreach my $row (@{$parser->getline_hr_all($db)}) {
my $entry = {};
$entry->{$_} = $row->{$_} foreach @fields;
push (@data,$entry);
}
close $db;
},
);
($inputs{$data_source} || $inputs{'default'})->();
}
my $conflict_count = 0;
{
my %identified; # $identified{$serial} == $node
ROW : foreach my $row (@data) {
my $valid = [
$config{'DeviceField'},
$config{'InterfaceField'},
];
foreach my $field (@$valid) { # Verify necessary fields aren't empty.
next ROW unless defined $row->{$field} and $row->{$field} =~ /\S+/; # Otherwise, skip to the next entry.
}
# Synchronize the entry with gathered info.
$conflict_count += synchronize ($nodes,\%identified,$auto_match,[$row]);
# Show the user how many nodes have been identified if necessary.
unless ($config{'Quiet'} or $config{'Verbose'}) {
print "\b"x$config{'DeviceOrder'};
printf ('%'.$config{'DeviceOrder'}.'d',scalar keys %identified);
}
}
# Show the user what's been found if necessary.
unless ($config{'Quiet'}) {
print scalar keys %identified if $config{'Verbose'};
print ' synchronized';
print ' ('.$conflict_count.' conflicts)' if $conflict_count > 0;
print "\n";
}
}
# Search for network devices and interfaces that were not identified in the database.
foreach my $ip (sort keys %$nodes) {
my $node = $nodes->{$ip};
foreach my $serial (sort keys %{$node->{'devices'}}) {
my $device = $node->{'devices'}{$serial};
unless ($device->{'identified'}) {
note ($config{'ConflictLog'},device_string ($device).' unidentified');
next;
}
foreach my $ifName (sort keys %{$device->{'interfaces'}}) {
my $interface = $device->{'interfaces'}{$ifName};
unless ($interface->{'identified'}) {
note ($config{'ConflictLog'},interface_string ($interface).' unidentified');
next;
}
}
}
}
}
################################################################################
=head2 update
push information to interfaces
B<Arguments>
I<( \%nodes )>
=over 3
=item nodes
the nodes to update
=back
B<Example>
C<update $nodes;>
Table
---------------------------------------------------------
| DeviceField | InterfaceField | InfoFields... |
--------------------------------------------------------- =============
| (serial) | (ifName) |(interface-specific)| --> || SyncOID ||
| ... | =============
--------------------------------------------------------- (device)
( run in 0.894 second using v1.01-cache-2.11-cpan-df04353d9ac )