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 )