IP address management for svc_acct, #19567
[freeside.git] / FS / FS / IP_Mixin.pm
diff --git a/FS/FS/IP_Mixin.pm b/FS/FS/IP_Mixin.pm
new file mode 100644 (file)
index 0000000..fdeb51d
--- /dev/null
@@ -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;