RT# 30783 Add network block enumerating utils
authorMitch Jackson <mitch@freeside.biz>
Mon, 25 Jun 2018 19:07:52 +0000 (14:07 -0500)
committerMitch Jackson <mitch@freeside.biz>
Mon, 25 Jun 2018 19:44:58 +0000 (14:44 -0500)
FS/FS/IP_Mixin.pm
FS/FS/addr_block.pm
FS/FS/svc_IP_Mixin.pm

index 3ec7693..2d69d9c 100644 (file)
@@ -266,9 +266,10 @@ sub router {
 
 =item used_addresses [ BLOCK ]
 
-Returns a list of all addresses (in BLOCK, or in all blocks)
-that are in use.  If called as an instance method, excludes 
-that instance from the search.
+Returns a list of all addresses that are in use by a service.  If called as an
+instance method, excludes that instance from the search.
+
+Does not filter by block, will return ALL used addresses. ref:f197bdbaa1
 
 =cut
 
@@ -283,6 +284,27 @@ sub _used_addresses {
   die "$class->_used_addresses not implemented";
 }
 
+=item used_addresses_in_block [ FS::addr_block ]
+
+Returns a list of all addresses in use within the given L<FS::addr_block>
+
+=cut
+
+sub used_addresses_in_block {
+  my ($self, $block) = @_;
+
+  (
+    $block->ip_gateway ? $block->ip_gateway : (),
+    $block->NetAddr->broadcast->addr,
+    map { $_->_used_addresses_in_block($block, $self ) } @subclasses
+  );
+}
+
+sub _used_addresses_in_block {
+  my $class = shift;
+  die "$class->_used_addresses_in_block not implemented";
+}
+
 =item is_used ADDRESS
 
 Returns a string describing what object is using ADDRESS, or 
index ba0f61d..fa0e42f 100755 (executable)
@@ -207,6 +207,23 @@ sub cidr {
   $self->NetAddr->cidr;
 }
 
+=item free_addrs
+
+Returns a sorted list of free addresses in the block.
+
+=cut
+
+sub free_addrs {
+  my $self = shift;
+
+  my %used_addr_map =
+    map {$_ => 1}
+    FS::IP_Mixin->used_addresses_in_block($self),
+    FS::Conf->new()->config('exclude_ip_addr');
+
+  grep { !exists $used_addr_map{$_} } map { $_->addr } $self->NetAddr->hostenum;
+}
+
 =item next_free_addr
 
 Returns a NetAddr::IP object corresponding to the first unassigned address 
@@ -416,4 +433,3 @@ now because that's the smallest block that makes any sense at all.
 =cut
 
 1;
-
index c89245f..4266a2a 100644 (file)
@@ -3,7 +3,8 @@ use base 'FS::IP_Mixin';
 
 use strict;
 use NEXT;
-use FS::Record qw(qsearchs qsearch);
+use Carp qw(croak carp);
+use FS::Record qw(qsearchs qsearch dbh);
 use FS::Conf;
 use FS::router;
 use FS::part_svc_router;
@@ -90,6 +91,9 @@ sub svc_ip_check {
 }
 
 sub _used_addresses {
+
+  # Returns all addresses in use.  Does not filter with $block. ref:f197bdbaa1
+
   my ($class, $block, $exclude) = @_;
   my $ip_field = $class->table_info->{'ip_field'}
     or return ();
@@ -107,6 +111,69 @@ sub _used_addresses {
     });
 }
 
+sub _used_addresses_in_block {
+  my ($class, $block) = @_;
+
+  croak "_used_addresses_in_block() requires an FS::addr_block parameter"
+    unless ref $block && $block->isa('FS::addr_block');
+
+  my $ip_field = $class->table_info->{'ip_field'};
+  if ( !$ip_field ) {
+    carp "_used_addresses_in_block() skipped, no ip_field";
+    return;
+  }
+
+  my $block_na = $block->NetAddr;
+
+  my $octets;
+  if ($block->ip_netmask >= 24) {
+    $octets = 3;
+  } elsif ($block->ip_netmask >= 16) {
+    $octets = 2;
+  } elsif ($block->ip_netmask >= 8) {
+    $octets = 1;
+  }
+
+  #  e.g.
+  # SELECT ip_addr
+  # FROM svc_broadband
+  # WHERE ip_addr != ''
+  #   AND ip_addr != '0e0'
+  #   AND ip_addr LIKE '10.0.2.%';
+  #
+  # For /24, /16 and /8 this approach is fast, even when svc_broadband table
+  # contains 650,000+ ip records.  For other allocations, this approach is
+  # not speedy, but usable.
+  #
+  # Note: A use case like this would could greatly benefit from a qsearch()
+  #       parameter to bypass FS::Record objects creation and just
+  #       return hashrefs from DBI.  200,000 hashrefs are many seconds faster
+  #       than 200,000 FS::Record objects
+  my %qsearch = (
+      table     => $class->table,
+      select    => $ip_field,
+      hashref   => { $ip_field => { op => '!=', value => '' }},
+      extra_sql => " AND $ip_field != '0e0' ",
+  );
+  if ( $octets ) {
+    my $block_str = join('.', (split(/\D/, $block_na->first))[0..$octets-1]);
+    $qsearch{extra_sql} .= " AND $ip_field LIKE ".dbh->quote("${block_str}.%");
+  }
+
+  if ( $block->ip_netmask % 8 ) {
+    # Some addresses returned by qsearch may be outside the network block,
+    # so each ip address is tested to be in the block before it's returned.
+    return
+      grep { $block_na->contains( NetAddr::IP->new( $_ ) ) }
+      map { $_->$ip_field }
+      qsearch( \%qsearch );
+  }
+
+  return
+    map { $_->$ip_field }
+    qsearch( \%qsearch );
+}
+
 sub _is_used {
   my ($class, $addr, $exclude) = @_;
   my $ip_field = $class->table_info->{'ip_field'}