7 use FS::Record qw(qsearch);
9 # careful about importing anything here--it will end up in a LOT of
12 use vars qw(@subclasses $DEBUG $conf);
16 # any subclass that can have IP addresses needs to be added here
17 @subclasses = (qw(FS::svc_broadband FS::svc_acct));
20 $conf ||= FS::Conf->new;
25 FS::IP_Mixin - Mixin class for objects that have IP addresses assigned.
29 The inheritor may provide the following methods:
33 =item ip_addr [ ADDRESS ]
35 Get/set the IP address, as a string. If the inheritor is also an
36 L<FS::Record> subclass and has an 'ip_addr' field, that field will be
37 used. Otherwise an C<ip_addr> method must be defined.
39 =item addr_block [ BLOCK ]
41 Get/set the address block, as an L<FS::addr_block> object. By default,
42 the 'blocknum' field will be used.
44 =item router [ ROUTER ]
46 Get/set the router, as an L<FS::router> object. By default, the
47 'routernum' field will be used. This is strictly optional; if present
48 the IP address can be assigned from all those available on a router,
49 rather than in a specific block.
51 =item _used_addresses [ BLOCK ]
53 Return a list of all addresses in use (within BLOCK, if it's specified).
54 The inheritor should cache this if possible.
56 =item _is_used ADDRESS
58 Test a specific address for availability. Should return an empty string
59 if it's free, or else a description of who or what is using it.
69 The method that should be called from check() in the subclass. This does
72 - In an C<auto_router> situation, sets the router and block to match the
74 - Otherwise, if the router and IP address are both set, validate the
75 choice of router and set the block correctly.
76 - Otherwise, if the router is set, assign an address (in the selected
77 block if there is one).
78 - Check the IP address for availability.
80 Returns an error if this fails for some reason (an address can't be
81 assigned from the requested router/block, or the requested address is
82 unavailable, or doesn't seem to be an IP address).
84 If router and IP address are both empty, this will do nothing. The
85 object's check() method should decide whether to allow a null IP address.
92 if ( $self->ip_addr eq '0.0.0.0' ) { #ipv6?
98 and $self->conf->exists('auto_router') ) {
99 # assign a router that matches this IP address
100 return $self->check_ip_addr || $self->assign_router;
102 if ( my $router = $self->router ) {
103 if ( $router->manual_addr ) {
104 # Router is set, and it's set to manual addressing, so
105 # clear blocknum and don't tamper with ip_addr.
106 $self->addr_block(undef);
108 my $block = $self->addr_block;
109 if ( !$block or !$block->manual_flag ) {
110 my $error = $self->assign_ip_addr;
111 return $error if $error;
113 # otherwise block is set to manual addressing
116 return $self->check_ip_addr;
121 Set the IP address to a free address in the selected block (C<addr_block>)
122 or router (C<router>) for this object. A block or router MUST be selected.
123 If the object already has an IP address and it is in that block/router's
124 address space, it won't be changed.
133 my $na = $self->NetAddr;
135 if ( $self->addr_block ) {
136 # choose an address in a specific block.
137 @blocks = ( $self->addr_block );
138 } elsif ( $self->router ) {
139 # choose an address from any block on a specific router.
140 @blocks = $self->router->auto_addr_block;
142 # what else should we do, search ALL blocks? that's crazy.
143 die "no block or router specified for assign_ip_addr\n";
148 foreach my $block (@blocks) {
149 if ( $self->ip_addr and $block->NetAddr->contains($na) ) {
152 # don't exit early on assigning a free address--check the rest of
153 # the blocks to see if the current address is in one of them.
155 $new_addr = $block->next_free_addr->addr;
160 return 'No IP address available on this router' unless $new_addr;
162 $self->ip_addr($new_addr);
163 $self->addr_block($new_block);
169 If the IP address is set, set the router and block accordingly. If there
170 is no block containing that address, returns an error.
176 return '' unless $self->ip_addr;
177 my $na = $self->NetAddr;
178 foreach my $router (qsearch('router', {})) {
179 foreach my $block ($router->addr_block) {
180 if ( $block->NetAddr->contains($na) ) {
181 $self->addr_block($block);
182 $self->router($router);
187 return $self->ip_addr . ' is not in an allowed block.';
192 Validate the IP address. Returns an empty string if it's correct and
193 available (or null), otherwise an error message.
199 my $addr = $self->ip_addr;
200 return '' if $addr eq '';
201 my $na = $self->NetAddr
202 or return "Can't parse address '$addr'";
203 if ( my $block = $self->addr_block ) {
204 if ( !$block->NetAddr->contains($na) ) {
205 return "Address $addr not in block ".$block->cidr;
208 # this returns '' if the address is in use by $self.
209 if ( my $dup = $self->is_used($self->ip_addr) ) {
210 return "Address $addr in use by $dup";
220 if ( defined $new ) {
221 die "addr_block() must take an address block"
222 unless $new->isa('FS::addr_block');
223 $self->blocknum($new->blocknum);
231 # could cache this...
232 FS::addr_block->by_key($self->blocknum);
239 if ( defined $new ) {
240 die "router() must take a router"
241 unless $new->isa('FS::router');
242 $self->routernum($new->routernum);
246 $self->routernum('');
250 FS::router->by_key($self->routernum);
253 =item used_addresses [ BLOCK ]
255 Returns a list of all addresses (in BLOCK, or in all blocks)
256 that are in use. If called as an instance method, excludes
257 that instance from the search.
264 return ( map { $_->_used_addresses($block, $self) } @subclasses );
267 sub _used_addresses {
269 die "$class->_used_addresses not implemented";
272 =item is_used ADDRESS
274 Returns a string describing what object is using ADDRESS, or
275 an empty string if it's not in use.
283 my $used = $_->_is_used($addr, $self);
284 return $used if $used;
291 die "$class->_is_used not implemented";
298 We can't reliably check for duplicate addresses across tables. A
299 more robust implementation would be to put all assigned IP addresses
300 in a single table with a unique index. We do a best-effort check
301 anyway, but it has a race condition.