From 2ee7f0c27233d800254d5244fc5913d881b48800 Mon Sep 17 00:00:00 2001 From: Mitch Jackson Date: Mon, 25 Jun 2018 14:07:52 -0500 Subject: [PATCH] RT# 30783 Add network block enumerating utils --- FS/FS/IP_Mixin.pm | 28 ++++++++++++++++++--- FS/FS/addr_block.pm | 18 +++++++++++++- FS/FS/svc_IP_Mixin.pm | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 5 deletions(-) diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm index 8920cebc5..07fa9e776 100644 --- a/FS/FS/IP_Mixin.pm +++ b/FS/FS/IP_Mixin.pm @@ -270,9 +270,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 @@ -287,6 +288,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 + +=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 diff --git a/FS/FS/addr_block.pm b/FS/FS/addr_block.pm index f07de490b..a39e1f1bb 100755 --- a/FS/FS/addr_block.pm +++ b/FS/FS/addr_block.pm @@ -238,6 +238,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 @@ -433,4 +450,3 @@ now because that's the smallest block that makes any sense at all. =cut 1; - diff --git a/FS/FS/svc_IP_Mixin.pm b/FS/FS/svc_IP_Mixin.pm index 8b2b5f17e..ce9218c10 100644 --- a/FS/FS/svc_IP_Mixin.pm +++ b/FS/FS/svc_IP_Mixin.pm @@ -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'} -- 2.11.0