diff options
author | Mark Wells <mark@freeside.biz> | 2012-10-30 12:16:17 -0700 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2012-10-30 12:16:17 -0700 |
commit | 87f255507af9f14dfbccd37eefd71a148f9af344 (patch) | |
tree | 7467e87ff6a27cdbe67fa60f4261e2d07a61f4b7 /FS/FS/IP_Mixin.pm | |
parent | d77fe06b27410a41855e1425114ab8d9cdae4ff0 (diff) |
IP address management for svc_acct, #19567
Diffstat (limited to 'FS/FS/IP_Mixin.pm')
-rw-r--r-- | FS/FS/IP_Mixin.pm | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm new file mode 100644 index 0000000..fdeb51d --- /dev/null +++ b/FS/FS/IP_Mixin.pm @@ -0,0 +1,305 @@ +package FS::IP_Mixin; + +use strict; +use NetAddr::IP; +use FS::addr_block; +use FS::router; +use FS::Record qw(qsearch); +use FS::Conf; +# careful about importing anything here--it will end up in a LOT of +# namespaces + +use vars qw(@subclasses $DEBUG $conf); + +$DEBUG = 0; + +# any subclass that can have IP addresses needs to be added here +@subclasses = (qw(FS::svc_broadband FS::svc_acct)); + +sub conf { + $conf ||= FS::Conf->new; +} + +=head1 NAME + +FS::IP_Mixin - Mixin class for objects that have IP addresses assigned. + +=head1 INTERFACE + +The inheritor may provide the following methods: + +=over 4 + +=item ip_addr [ ADDRESS ] + +Get/set the IP address, as a string. If the inheritor is also an +L<FS::Record> subclass and has an 'ip_addr' field, that field will be +used. Otherwise an C<ip_addr> method must be defined. + +=item addr_block [ BLOCK ] + +Get/set the address block, as an L<FS::addr_block> object. By default, +the 'blocknum' field will be used. + +=item router [ ROUTER ] + +Get/set the router, as an L<FS::router> object. By default, the +'routernum' field will be used. This is strictly optional; if present +the IP address can be assigned from all those available on a router, +rather than in a specific block. + +=item _used_addresses [ BLOCK ] + +Return a list of all addresses in use (within BLOCK, if it's specified). +The inheritor should cache this if possible. + +=item _is_used ADDRESS + +Test a specific address for availability. Should return an empty string +if it's free, or else a description of who or what is using it. + +=back + +=head1 METHODS + +=over 4 + +=item ip_check + +The method that should be called from check() in the subclass. This does +the following: + +- In an C<auto_router> situation, sets the router and block to match the + object's IP address. +- Otherwise, if the router and IP address are both set, validate the + choice of router and set the block correctly. +- Otherwise, if the router is set, assign an address (in the selected + block if there is one). +- Check the IP address for availability. + +Returns an error if this fails for some reason (an address can't be +assigned from the requested router/block, or the requested address is +unavailable, or doesn't seem to be an IP address). + +If router and IP address are both empty, this will do nothing. The +object's check() method should decide whether to allow a null IP address. + +=cut + +sub ip_check { + my $self = shift; + + if ( $self->ip_addr eq '0.0.0.0' ) { #ipv6? + $self->ip_addr(''); + } + + if ( $self->ip_addr + and !$self->router + and $self->conf->exists('auto_router') ) { + # assign a router that matches this IP address + return $self->check_ip_addr || $self->assign_router; + } + if ( my $router = $self->router ) { + if ( $router->manual_addr ) { + # Router is set, and it's set to manual addressing, so + # clear blocknum and don't tamper with ip_addr. + $self->addr_block(undef); + } else { + my $block = $self->addr_block; + if ( !$block or !$block->manual_flag ) { + my $error = $self->assign_ip_addr; + return $error if $error; + } + # otherwise block is set to manual addressing + } + } + return $self->check_ip_addr; +} + +=item assign_ip_addr + +Set the IP address to a free address in the selected block (C<addr_block>) +or router (C<router>) for this object. A block or router MUST be selected. +If the object already has an IP address and it is in that block/router's +address space, it won't be changed. + +=cut + +sub assign_ip_addr { + my $self = shift; + my %opt = @_; + + my @blocks; + my $na = $self->NetAddr; + + if ( $self->addr_block ) { + # choose an address in a specific block. + @blocks = ( $self->addr_block ); + } elsif ( $self->router ) { + # choose an address from any block on a specific router. + @blocks = $self->router->auto_addr_block; + } else { + # what else should we do, search ALL blocks? that's crazy. + die "no block or router specified for assign_ip_addr\n"; + } + + my $new_addr; + my $new_block; + foreach my $block (@blocks) { + if ( $self->ip_addr and $block->NetAddr->contains($na) ) { + return ''; + } + # don't exit early on assigning a free address--check the rest of + # the blocks to see if the current address is in one of them. + if (!$new_addr) { + $new_addr = $block->next_free_addr->addr; + $new_block = $block; + } + } + + return 'No IP address available on this router' unless $new_addr; + + $self->ip_addr($new_addr); + $self->addr_block($new_block); + ''; +} + +=item assign_router + +If the IP address is set, set the router and block accordingly. If there +is no block containing that address, returns an error. + +=cut + +sub assign_router { + my $self = shift; + return '' unless $self->ip_addr; + my $na = $self->NetAddr; + foreach my $router (qsearch('router', {})) { + foreach my $block ($router->addr_block) { + if ( $block->NetAddr->contains($na) ) { + $self->addr_block($block); + $self->router($router); + return ''; + } + } + } + return $self->ip_addr . ' is not in an allowed block.'; +} + +=item check_ip_addr + +Validate the IP address. Returns an empty string if it's correct and +available (or null), otherwise an error message. + +=cut + +sub check_ip_addr { + my $self = shift; + my $addr = $self->ip_addr; + return '' if $addr eq ''; + my $na = $self->NetAddr + or return "Can't parse address '$addr'"; + 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 ( my $dup = $self->is_used($self->ip_addr) ) { + return "Address $addr in use by $dup"; + } + ''; +} + +# sensible defaults +sub addr_block { + my $self = shift; + if ( @_ ) { + my $new = shift; + if ( defined $new ) { + die "addr_block() must take an address block" + unless $new->isa('FS::addr_block'); + $self->blocknum($new->blocknum); + return $new; + } else { + #$new is undef + $self->blocknum(''); + return undef; + } + } + # could cache this... + FS::addr_block->by_key($self->blocknum); +} + +sub router { + my $self = shift; + if ( @_ ) { + my $new = shift; + if ( defined $new ) { + die "router() must take a router" + unless $new->isa('FS::router'); + $self->routernum($new->routernum); + return $new; + } else { + #$new is undef + $self->routernum(''); + return undef; + } + } + FS::router->by_key($self->routernum); +} + +=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. + +=cut + +sub used_addresses { + my $self = shift; + my $block = shift; + return ( map { $_->_used_addresses($block, $self) } @subclasses ); +} + +sub _used_addresses { + my $class = shift; + die "$class->_used_addresses not implemented"; +} + +=item is_used ADDRESS + +Returns a string describing what object is using ADDRESS, or +an empty string if it's not in use. + +=cut + +sub is_used { + my $self = shift; + my $addr = shift; + for (@subclasses) { + my $used = $_->_is_used($addr, $self); + return $used if $used; + } + ''; +} + +sub _is_used { + my $class = shift; + die "$class->_is_used not implemented"; +} + +=back + +=head1 BUGS + +We can't reliably check for duplicate addresses across tables. A +more robust implementation would be to put all assigned IP addresses +in a single table with a unique index. We do a best-effort check +anyway, but it has a race condition. + +=cut + +1; |