The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Vulcan::NetMap;

=head1 NAME

Vulcan::NetMap - network mappings of data centers

=head1 SYNOPSIS

 use Vulcan::NetMap;

 my $map = Vulcan::NetMap->load( '/conf/file' );
 my $info = $map->search( $ip );

=cut
use strict;
use warnings;

use Carp;
use YAML::XS;

use constant { MAX => 32 };

sub new { load( @_ ) };

=head1 CONFIG

A YAML file, containing a HASH of subnet definitions indexed by data centers.
Each subnet definition should be a HASH of masks indexed by subnets. e.g.

 ---
 dc1:
   10.141.0.0: 255.255.0.0
   111.13.65.1: 255.255.255.0
 dc2:
   10.138.0.0: 16
   10.139.0.0: 16
   106.120.160.0: 24

=cut
sub load
{
    my ( $class, $conf ) = splice @_;
    my ( $error, %mask, %self ) = "Invalid config: $conf";

    eval { $conf = YAML::XS::LoadFile $conf };
    confess "$error: $@" if $@;
    confess "$error: not HASH" if ref $conf ne 'HASH';

    while ( my ( $dc, $conf ) = each %$conf )
    {
        my $error = "$error: $dc";
        confess "$error: not HASH" if ref $conf ne 'HASH';

        while ( my ( $net, $mask ) = each %$conf )
        {
            my $error = "$error: $net";

            confess "$error: invalid subnet" unless $net = $class->ip2n( $net );
            confess "$error: invalid netmask"
                unless $mask{$mask} ||= $class->mask( $mask );

            $self{$dc}{ $net & $mask{$mask} } = $mask{$mask};
        }
    }
    bless \%self, ref $class || $class;
}

=head1 METHODS

=head3 dc( %param )

Returns a list of data centers in the config.
List is sorted by I<size> or I<segment> if $param{sort} is defined.

=cut
sub dc
{
    my ( $self, %param ) = splice @_;
    my @dc = sort keys %$self;
    
    if ( my $sort = $param{sort} )
    {
        my %size;
        if ( $sort =~ /size/i )
        {
            my $mask = 2 ** MAX - 1;
            for my $dc ( @dc )
            {
                my $size = 0;
                map { $size += $_ ^ $mask } values %{ $self->{$dc} };
                $size{$dc} = $size;
            }
        }
        elsif ( $sort =~ /seg/i )
        {
            %size = map { $_ => 0 + keys %{ $self->{$_} } } @dc;
        }
        @dc = sort { $size{$a} <=> $size{$b} } keys %size;
    }
    return wantarray ? @dc : \@dc;
}

=head3 search( $ip, %param )

Returns I<dc>, I<net>, I<mask> of $ip if within the net map.
I<net> and I<mask> are returned in integer if $param{int} is defined. 

=cut
sub search
{
    my ( $self, $ip, %param ) = splice @_;
    my %info;

    if ( $ip = $self->ip2n( $ip ) )
    {
        while ( my ( $dc, $conf ) = each %$self )
        {
            my %mask = reverse %$conf;
            map { my $net = $ip & $_; last if %info = ( $_ = $conf->{$net} )
                ? ( dc => $dc, net => $net, mask => $_ ) : () } keys %mask;
        }
    }

    map { $info{$_} = $self->n2ip( $info{$_} ) } qw( net mask )
        if %info && ! $param{int};

    return wantarray ? %info : keys %info ? \%info : undef;
}

=head3 mask( $mask )

Returns the integer value of a (dotted or decimal) netmask.

=cut
sub mask
{
    my ( $class, $mask ) = splice @_;

    return undef unless $mask = $mask !~ /^\d+$/
        ? $class->ip2n( $mask ) : $mask < MAX
        ? ( 2 ** $mask - 1 ) << ( MAX - $mask ) : undef;
    return split( /0+/, sprintf '%032b', $mask ) > 1 ? undef : $mask;
}

=head3 ip2n( $ip )

Returns the integer value of a dotted ip.

=cut
sub ip2n
{
    my ( $class, $ip ) = splice @_;
    return undef unless my @ip = $ip =~ qr/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/o;
    return unpack N => pack C4 => @ip;
}

=head3 n2ip( $int )

Returns dotted ip form of an integer.

=cut
sub n2ip
{
    my ( $class, $ip ) = splice @_;
    return $ip =~ /^\d+$/ ? join '.', unpack C4 => pack N => $ip : undef;
}

1;