RT# 30783 Add network block enumerating utils
[freeside.git] / FS / FS / IP_Mixin.pm
1 package FS::IP_Mixin;
2
3 use strict;
4 use NetAddr::IP;
5 use FS::addr_block;
6 use FS::router;
7 use FS::addr_range;
8 use FS::Record qw(qsearch);
9 use FS::Conf;
10 # careful about importing anything here--it will end up in a LOT of 
11 # namespaces
12
13 use vars qw(@subclasses $DEBUG $conf);
14
15 $DEBUG = 0;
16
17 # any subclass that can have IP addresses needs to be added here
18 @subclasses = (qw(FS::svc_broadband FS::svc_acct));
19
20 sub conf {
21   $conf ||= FS::Conf->new;
22 }
23
24 =head1 NAME
25
26 FS::IP_Mixin - Mixin class for objects that have IP addresses assigned.
27
28 =head1 INTERFACE
29
30 The inheritor may provide the following methods:
31
32 =over 4
33
34 =item ip_addr [ ADDRESS ]
35
36 Get/set the IP address, as a string.  If the inheritor is also an
37 L<FS::Record> subclass and has an 'ip_addr' field, that field will be 
38 used.  Otherwise an C<ip_addr> method must be defined.
39
40 =item addr_block [ BLOCK ]
41
42 Get/set the address block, as an L<FS::addr_block> object.  By default,
43 the 'blocknum' field will be used.
44
45 =item router [ ROUTER ]
46
47 Get/set the router, as an L<FS::router> object.  By default, the 
48 'routernum' field will be used.  This is strictly optional; if present
49 the IP address can be assigned from all those available on a router, 
50 rather than in a specific block.
51
52 =item _used_addresses [ BLOCK ]
53
54 Return a list of all addresses in use (within BLOCK, if it's specified).
55 The inheritor should cache this if possible.
56
57 =item _is_used ADDRESS
58
59 Test a specific address for availability.  Should return an empty string
60 if it's free, or else a description of who or what is using it.
61
62 =back
63
64 =head1 METHODS
65
66 =over 4
67
68 =item ip_check
69
70 The method that should be called from check() in the subclass.  This does 
71 the following:
72
73 - In an C<auto_router> situation, sets the router and block to match the 
74   object's IP address.
75 - Otherwise, if the router and IP address are both set, validate the 
76   choice of router and set the block correctly.
77 - Otherwise, if the router is set, assign an address (in the selected
78   block if there is one).
79 - Check the IP address for availability.
80
81 Returns an error if this fails for some reason (an address can't be 
82 assigned from the requested router/block, or the requested address is
83 unavailable, or doesn't seem to be an IP address).
84
85 If router and IP address are both empty, this will do nothing.  The 
86 object's check() method should decide whether to allow a null IP address.
87
88 =cut
89
90 sub ip_check {
91   my $self = shift;
92
93   if ( $self->ip_addr eq '0.0.0.0' ) { #ipv6?
94     $self->ip_addr('');
95   }
96
97   if ( $self->ip_addr
98        and !$self->router
99        and $self->conf->exists('auto_router') ) {
100     # assign a router that matches this IP address
101     return $self->check_ip_addr || $self->assign_router;
102   }
103   if ( my $router = $self->router ) {
104     if ( $router->manual_addr ) {
105       # Router is set, and it's set to manual addressing, so 
106       # clear blocknum and don't tamper with ip_addr.
107       $self->addr_block(undef);
108     } else {
109       my $block = $self->addr_block;
110       if ( !$block or !$block->manual_flag ) {
111         my $error = $self->assign_ip_addr;
112         return $error if $error;
113       }
114       # otherwise block is set to manual addressing
115     }
116   }
117   return $self->check_ip_addr;
118 }
119
120 =item assign_ip_addr
121
122 Set the IP address to a free address in the selected block (C<addr_block>)
123 or router (C<router>) for this object.  A block or router MUST be selected.
124 If the object already has an IP address and it is in that block/router's 
125 address space, it won't be changed.
126
127 =cut
128
129 sub assign_ip_addr {
130   my $self = shift;
131   my %opt = @_;
132
133   #otherwise we'll get the same assignment for concurrent identical calls
134   # this will serialize them
135   $_->lock_table foreach @subclasses;
136
137   my @blocks;
138   my $na = $self->NetAddr;
139
140   if ( $self->addr_block ) {
141     # choose an address in a specific block.
142     @blocks = ( $self->addr_block );
143   } elsif ( $self->router ) {
144     # choose an address from any block on a specific router.
145     @blocks = $self->router->auto_addr_block;
146   } else {
147     # what else should we do, search ALL blocks? that's crazy.
148     die "no block or router specified for assign_ip_addr\n";
149   }
150
151   my $new_addr;
152   my $new_block;
153   foreach my $block (@blocks) {
154     if ( $self->ip_addr and $block->NetAddr->contains($na) ) {
155       return '';
156     }
157     # don't exit early on assigning a free address--check the rest of 
158     # the blocks to see if the current address is in one of them.
159     if (!$new_addr) {
160       $new_addr = $block->next_free_addr;
161       $new_block = $block;
162     }
163   }
164  
165   return 'No IP address available on this router' unless $new_addr;
166
167   $self->ip_addr($new_addr->addr);
168   $self->addr_block($new_block);
169   '';
170 }
171
172 =item assign_router
173
174 If the IP address is set, set the router and block accordingly.  If there
175 is no block containing that address, returns an error.
176
177 =cut
178
179 sub assign_router {
180   my $self = shift;
181   return '' unless $self->ip_addr;
182   my $na = $self->NetAddr;
183   foreach my $router (qsearch('router', {})) {
184     foreach my $block ($router->addr_block) {
185       if ( $block->NetAddr->contains($na) ) {
186         $self->addr_block($block);
187         $self->router($router);
188         return '';
189       }
190     }
191   }
192   return $self->ip_addr . ' is not in an allowed block.';
193 }
194
195 =item check_ip_addr
196
197 Validate the IP address.  Returns an empty string if it's correct and 
198 available (or null), otherwise an error message.
199
200 =cut
201
202 sub check_ip_addr {
203   my $self = shift;
204   my $addr = $self->ip_addr;
205   return '' if $addr eq '';
206   my $na = $self->NetAddr
207     or return "Can't parse address '$addr'";
208   # if there's a chosen address block, check that the address is in it
209   if ( my $block = $self->addr_block ) {
210     if ( !$block->NetAddr->contains($na) ) {
211       return "Address $addr not in block ".$block->cidr;
212     }
213   }
214   # if the address is in any designated ranges, check that they don't 
215   # disallow use
216   foreach my $range (FS::addr_range->any_contains($addr)) {
217     if ( !$range->allow_use ) {
218       return "Address $addr is in ".$range->desc." range ".$range->as_string;
219     }
220   }
221   # check that nobody else is sitting on the address
222   # (this returns '' if the address is in use by $self)
223   if ( my $dup = $self->is_used($self->ip_addr) ) {
224     return "Address $addr in use by $dup";
225   }
226   '';
227 }
228
229 # sensible defaults
230 sub addr_block {
231   my $self = shift;
232   if ( @_ ) {
233     my $new = shift;
234     if ( defined $new ) {
235       die "addr_block() must take an address block"
236         unless $new->isa('FS::addr_block');
237       $self->blocknum($new->blocknum);
238       return $new;
239     } else {
240       #$new is undef
241       $self->blocknum('');
242       return undef;
243     }
244   }
245   # could cache this...
246   FS::addr_block->by_key($self->blocknum);
247 }
248
249 sub router {
250   my $self = shift;
251   if ( @_ ) {
252     my $new = shift;
253     if ( defined $new ) {
254       die "router() must take a router"
255         unless $new->isa('FS::router');
256       $self->routernum($new->routernum);
257       return $new;
258     } else {
259       #$new is undef
260       $self->routernum('');
261       return undef;
262     }
263   }
264   FS::router->by_key($self->routernum);
265 }
266
267 =item used_addresses [ BLOCK ]
268
269 Returns a list of all addresses that are in use by a service.  If called as an
270 instance method, excludes that instance from the search.
271
272 Does not filter by block, will return ALL used addresses. ref:f197bdbaa1
273
274 =cut
275
276 sub used_addresses {
277   my $self = shift;
278   my $block = shift;
279   return ( map { $_->_used_addresses($block, $self) } @subclasses );
280 }
281
282 sub _used_addresses {
283   my $class = shift;
284   die "$class->_used_addresses not implemented";
285 }
286
287 =item used_addresses_in_block [ FS::addr_block ]
288
289 Returns a list of all addresses in use within the given L<FS::addr_block>
290
291 =cut
292
293 sub used_addresses_in_block {
294   my ($self, $block) = @_;
295
296   (
297     $block->ip_gateway ? $block->ip_gateway : (),
298     $block->NetAddr->broadcast->addr,
299     map { $_->_used_addresses_in_block($block, $self ) } @subclasses
300   );
301 }
302
303 sub _used_addresses_in_block {
304   my $class = shift;
305   die "$class->_used_addresses_in_block not implemented";
306 }
307
308 =item is_used ADDRESS
309
310 Returns a string describing what object is using ADDRESS, or 
311 an empty string if it's not in use.
312
313 =cut
314
315 sub is_used {
316   my $self = shift;
317   my $addr = shift;
318   for (@subclasses) {
319     my $used = $_->_is_used($addr, $self);
320     return $used if $used;
321   }
322   '';
323 }
324
325 sub _is_used {
326   my $class = shift;
327   die "$class->_is_used not implemented";
328 }
329
330 =back
331
332 =head1 BUGS
333
334 We can't reliably check for duplicate addresses across tables.  A 
335 more robust implementation would be to put all assigned IP addresses
336 in a single table with a unique index.  We do a best-effort check 
337 anyway, but it has a race condition.
338
339 =cut
340
341 1;