diff options
author | Mark Wells <mark@freeside.biz> | 2013-12-10 20:08:43 -0800 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2013-12-10 20:08:43 -0800 |
commit | a4c1077430ac3b053c30084dcf76c54be45dca08 (patch) | |
tree | 2f225a7519cc9f0f83b4b7b99986b99cdb37f2e3 /FS | |
parent | c27f80ec10180391d00286bf50dfbf09a96c1b00 (diff) |
Designate forbidden address ranges, #25530
Diffstat (limited to 'FS')
-rw-r--r-- | FS/FS/IP_Mixin.pm | 11 | ||||
-rw-r--r-- | FS/FS/Mason.pm | 1 | ||||
-rw-r--r-- | FS/FS/Schema.pm | 12 | ||||
-rw-r--r-- | FS/FS/addr_range.pm | 264 | ||||
-rw-r--r-- | FS/MANIFEST | 2 | ||||
-rw-r--r-- | FS/t/addr_range.t | 5 |
6 files changed, 294 insertions, 1 deletions
diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm index fdeb51d..b3c1052 100644 --- a/FS/FS/IP_Mixin.pm +++ b/FS/FS/IP_Mixin.pm @@ -200,12 +200,21 @@ sub check_ip_addr { return '' if $addr eq ''; my $na = $self->NetAddr or return "Can't parse address '$addr'"; + # if there's a chosen address block, check that the address is in it if ( my $block = $self->addr_block ) { if ( !$block->NetAddr->contains($na) ) { return "Address $addr not in block ".$block->cidr; } } - # this returns '' if the address is in use by $self. + # if the address is in any designated ranges, check that they don't + # disallow use + foreach my $range (FS::addr_range->any_contains($addr)) { + if ( !$range->allow_use ) { + return "Address $addr is in ".$range->desc." range ".$range->as_string; + } + } + # check that nobody else is sitting on the address + # (this returns '' if the address is in use by $self) if ( my $dup = $self->is_used($self->ip_addr) ) { return "Address $addr in use by $dup"; } diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index fc25a86..fefa1bc 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -358,6 +358,7 @@ if ( -e $addl_handler_use_file ) { use FS::cable_provider; use FS::cust_credit_void; use FS::discount_class; + use FS::addr_range; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 6403782..647e2b1 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -4278,6 +4278,18 @@ sub tables_hashref { ], }, + 'addr_range' => { + 'columns' => [ + 'rangenum', 'serial', '', '', '', '', + 'start', 'varchar', '', 15, '', '', + 'length', 'int', '', '', '', '', + 'status', 'varchar', 'NULL', 32, '', '', + ], + 'primary_key' => 'rangenum', + 'unique' => [], + 'index' => [], + }, + 'svc_broadband' => { 'columns' => [ 'svcnum', 'int', '', '', '', '', diff --git a/FS/FS/addr_range.pm b/FS/FS/addr_range.pm new file mode 100644 index 0000000..5faa443 --- /dev/null +++ b/FS/FS/addr_range.pm @@ -0,0 +1,264 @@ +package FS::addr_range; + +use strict; +use base qw( FS::Record ); +use vars qw( %status_desc + %status_allow_auto + %status_allow_use + ); +use FS::Record qw( qsearch qsearchs ); +use NetAddr::IP; + +# metadata about status strings: +# how to describe them +%status_desc = ( + '' => '', + 'unavailable' => 'unavailable', +); + +# whether addresses in this range are available for use +%status_allow_use = ( + '' => 1, + 'unavailable' => 0, +); + +=head1 NAME + +FS::addr_range - Object methods for addr_range records + +=head1 SYNOPSIS + + use FS::addr_range; + + $record = new FS::addr_range \%hash; + $record = new FS::addr_range { 'column' => 'value' }; + + $error = $record->insert; + + $error = $new_record->replace($old_record); + + $error = $record->delete; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::addr_range object represents a contiguous range of IP +addresses assigned to a certain purpose. Unlike L<FS::addr_block>, +this isn't a routing block; the range doesn't have to be aligned on +a subnet boundary, and doesn't have a gateway or broadcast address. +It's just a range. + +=over 4 + +=item rangenum - primary key + +=item start - starting address of the range, as a dotted quad + +=item length - number of addresses in the range, including start + +=item status - what to do with the addresses in this range; currently can +only be "unavailable", which makes the addresses unavailable for assignment +to any kind of service. + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new range. To add the example to the database, see L<"insert">. + +Note that this stores the hash reference, not a distinct copy of the hash it +points to. You can ask the object for a copy with the I<hash> method. + +=cut + +sub table { 'addr_range'; } + +=item insert + +Adds this record to the database. If there is an error, returns the error, +otherwise returns false. + +=cut + +# the insert method can be inherited from FS::Record + +=item delete + +Delete this record from the database. + +=cut + +# the delete method can be inherited from FS::Record + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error, otherwise returns false. + +=cut + +# the replace method can be inherited from FS::Record + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('rangenum') + || $self->ut_ip('start') + || $self->ut_number('length') + || $self->ut_textn('status') + ; + return $error if $error; + + $self->SUPER::check; +} + +=item end [ IPADDR ] + +Get/set the end IP address in the range. This isn't actually part of the +record but it's convenient. + +=cut + +sub end { + my $self = shift; + # if there's no start address, just return nothing + my $start = NetAddr::IP->new($self->start, 0) or return ''; + + my $new = shift; + if ( $new ) { + my $end = NetAddr::IP->new($new, 0) + or die "bad end address $new"; + if ( $end < $start ) { + $self->set('start', $end); + ($end, $start) = ($start, $end); + } + $self->set('length', $end - $start + 1); + return $end->addr; + } + my $end = $start + $self->get('length') - 1; + $end->addr; +} + +=item contains IPADDR + +Checks whether IPADDR (a dotted-quad IPv4 address) is within the range. + +=cut + +sub contains { + my $self = shift; + my $addr = shift; + $addr = NetAddr::IP->new($addr, 0) + unless ref($addr) and UNIVERSAL::isa($addr, 'NetAddr::IP'); + return 0 unless $addr; + + my $start = NetAddr::IP->new($self->start, 0); + + return ($addr >= $start and $addr - $start < $self->length) ? 1 : 0; +} + +=item as_string + +Returns a readable string showing the address range. + +=cut + +sub as_string { + my $self = shift; + my $start = NetAddr::IP->new($self->start, 0); + my $end = $start + $self->length; + + if ( $self->length == 1 ) { + # then just the address + return $self->start; + } else { # we have to get tricksy + my @end_octets = split('\.', $end->addr); + $start = ($start->numeric)[0] + 0; + $end = ($end->numeric)[0] + 0; + # which octets are different between start and end? + my $delta = $end ^ $start; + foreach (0xffffff, 0xffff, 0xff) { + if ( $delta <= $_ ) { + # then they are identical in the first 8/16/24 bits + shift @end_octets; + } + } + return $self->start . '-' . join('.', @end_octets); + } +} + +=item desc + +Returns a semi-friendly description of the block status. + +=item allow_use + +Returns true if addresses in this range can be used by services, etc. + +=cut + +sub desc { + my $self = shift; + $status_desc{ $self->status }; +} + +sub allow_auto { + my $self = shift; + $status_allow_auto{ $self->status }; +} + +sub allow_use { + my $self = shift; + $status_allow_use{ $self->status }; +} + +=back + +=head1 CLASS METHODS + +=sub any_contains IPADDR + +Returns all address ranges that contain IPADDR. + +=cut + +sub any_contains { + my $self = shift; + my $addr = shift; + return grep { $_->contains($addr) } qsearch('addr_range', {}); +} + +=head1 DEVELOPER NOTE + +L<NetAddr::IP> objects have netmasks. When using them to represent +range endpoints, be sure to set the netmask to I<zero> so that math on +the address doesn't stop at the subnet boundary. (The default is /32, +which doesn't work very well. Address ranges ignore subnet boundaries. + +=head1 BUGS + +=head1 SEE ALSO + +L<FS::svc_IP_Mixin>, L<FS::Record>, schema.html from the base documentation. + +=cut + +1; + diff --git a/FS/MANIFEST b/FS/MANIFEST index e635831..0b36e24 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -729,3 +729,5 @@ FS/cust_credit_void.pm t/cust_credit_void.t FS/discount_class.pm t/discount_class.t +FS/addr_range.pm +t/addr_range.t diff --git a/FS/t/addr_range.t b/FS/t/addr_range.t new file mode 100644 index 0000000..6747d67 --- /dev/null +++ b/FS/t/addr_range.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::addr_range; +$loaded=1; +print "ok 1\n"; |