RT# 30783 Improve speed of ip address auto-assignment
[freeside.git] / FS / FS / addr_block.pm
1 package FS::addr_block;
2 use base qw(FS::Record);
3
4 use strict;
5 use Carp qw( carp );
6 use List::Util qw( first );
7 use NetAddr::IP;
8 use FS::Conf;
9 use FS::Record qw( qsearch dbh ); #qsearchs
10 use FS::IP_Mixin;
11 use FS::addr_range;
12
13 =head1 NAME
14
15 FS::addr_block - Object methods for addr_block records
16
17 =head1 SYNOPSIS
18
19   use FS::addr_block;
20
21   $record = new FS::addr_block \%hash;
22   $record = new FS::addr_block { 'column' => 'value' };
23
24   $error = $record->insert;
25
26   $error = $new_record->replace($old_record);
27
28   $error = $record->delete;
29
30   $error = $record->check;
31
32 =head1 DESCRIPTION
33
34 An FS::addr_block record describes an address block assigned for broadband 
35 access.  FS::addr_block inherits from FS::Record.  The following fields are 
36 currently supported:
37
38 =over 4
39
40 =item blocknum - primary key, used in FS::svc_broadband to associate 
41 services to the block.
42
43 =item routernum - the router (see FS::router) to which this 
44 block is assigned.
45
46 =item ip_gateway - the gateway address used by customers within this block.  
47
48 =item ip_netmask - the netmask of the block, expressed as an integer.
49
50 =item manual_flag - prohibit automatic ip assignment from this block when true. 
51
52 =item agentnum - optional agent number (see L<FS::agent>)
53
54 =back
55
56 =head1 METHODS
57
58 =over 4
59
60 =item new HASHREF
61
62 Create a new record.  To add the record to the database, see "insert".
63
64 =cut
65
66 sub table { 'addr_block'; }
67
68 =item insert
69
70 Adds this record to the database.  If there is an error, returns the error,
71 otherwise returns false.
72
73 =item delete
74
75 Deletes this record from the database.  If there is an error, returns the
76 error, otherwise returns false.
77
78 =cut
79
80 sub delete {
81   my $self = shift;
82   return 'Block must be deallocated and have no services before deletion'
83     if $self->router || $self->svc_broadband;
84
85     local $SIG{HUP} = 'IGNORE';
86     local $SIG{INT} = 'IGNORE';
87     local $SIG{QUIT} = 'IGNORE';
88     local $SIG{TERM} = 'IGNORE';
89     local $SIG{TSTP} = 'IGNORE';
90     local $SIG{PIPE} = 'IGNORE';
91
92     my $oldAutoCommit = $FS::UID::AutoCommit;
93     local $FS::UID::AutoCommit = 0;
94     my $dbh = dbh;
95     
96     my $error = $self->SUPER::delete;
97     if ( $error ) {
98        $dbh->rollback if $oldAutoCommit;
99        return $error;
100     }
101   
102     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
103     '';
104 }
105
106
107 =item replace OLD_RECORD
108
109 Replaces OLD_RECORD with this one in the database.  If there is an error,
110 returns the error, otherwise returns false.
111
112 At present it's not possible to reallocate a block to a different router 
113 except by deallocating it first, which requires that none of its addresses 
114 be assigned.  This is probably as it should be.
115
116 sub replace_check {
117   my ( $new, $old ) = ( shift, shift );
118
119   unless($new->routernum == $old->routernum) {
120     my @svc = $self->svc_broadband;
121     if (@svc) {
122       return 'Block has assigned addresses: '.
123              join ', ', map {$_->ip_addr} @svc;
124     }
125
126     return 'Block is already allocated'
127       if($new->routernum && $old->routernum);
128
129   }
130
131   '';
132 }
133
134 =item check
135
136 Checks all fields to make sure this is a valid record.  If there is an error,
137 returns the error, otherwise returns false.  Called by the insert and replace
138 methods.
139
140 =cut
141
142 sub check {
143   my $self = shift;
144
145   my $error =
146     $self->ut_number('routernum')
147     || $self->ut_ip('ip_gateway')
148     || $self->ut_number('ip_netmask')
149     || $self->ut_enum('manual_flag', [ '', 'Y' ])
150     || $self->ut_agentnum_acl('agentnum', 'Broadband global configuration')
151   ;
152   return $error if $error;
153
154
155   # A routernum of 0 indicates an unassigned block and is allowed
156   return "Unknown routernum"
157     if ($self->routernum and not $self->router);
158
159   my $self_addr = $self->NetAddr;
160   return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask
161     unless $self_addr;
162
163   if (not $self->blocknum) {
164     my @block = grep {
165       my $block_addr = $_->NetAddr;
166       if($block_addr->contains($self_addr) 
167       or $self_addr->contains($block_addr)) { $_; };
168     } qsearch( 'addr_block', {});
169     foreach(@block) {
170       return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask;
171     }
172   }
173
174   $self->SUPER::check;
175 }
176
177
178 =item router
179
180 Returns the FS::router object corresponding to this object.  If the 
181 block is unassigned, returns undef.
182
183 =item svc_broadband
184
185 Returns a list of FS::svc_broadband objects associated
186 with this object.
187
188 =item NetAddr
189
190 Returns a NetAddr::IP object for this block's address and netmask.
191
192 =cut
193
194 sub NetAddr {
195   my $self = shift;
196   new NetAddr::IP ($self->ip_gateway, $self->ip_netmask);
197 }
198
199 =item cidr
200
201 Returns a CIDR string for this block's address and netmask, i.e. 10.4.20.0/24
202
203 =cut
204
205 sub cidr {
206   my $self = shift;
207   $self->NetAddr->cidr;
208 }
209
210 =item free_addrs
211
212 Returns a sorted list of free addresses in the block.
213
214 =cut
215
216 sub free_addrs {
217   my $self = shift;
218
219   my %used_addr_map =
220     map {$_ => 1}
221     FS::IP_Mixin->used_addresses_in_block($self),
222     FS::Conf->new()->config('exclude_ip_addr');
223
224   grep { !exists $used_addr_map{$_} } map { $_->addr } $self->NetAddr->hostenum;
225 }
226
227 =item next_free_addr
228
229 Returns a NetAddr::IP object corresponding to the first unassigned address 
230 in the block (other than the network, broadcast, or gateway address).  If 
231 there are no free addresses, returns nothing.  There are never free addresses
232 when manual_flag is true.
233
234 There is no longer a method to return all free addresses in a block.
235
236 =cut
237
238 sub next_free_addr {
239   my $self = shift;
240   my $selfaddr = $self->NetAddr;
241
242   return () if $self->manual_flag;
243
244   my $conf = new FS::Conf;
245   my @excludeaddr = $conf->config('exclude_ip_addr');
246
247   my %used = map { $_ => 1 }
248   (
249     @excludeaddr,
250     $selfaddr->addr,
251     $selfaddr->network->addr,
252     $selfaddr->broadcast->addr,
253     FS::IP_Mixin->used_addresses_in_block($self)
254   );
255
256   # just do a linear search of the block
257   my $freeaddr = $selfaddr->network + 1;
258   while ( $freeaddr < $selfaddr->broadcast ) {
259     # also make sure it's not blocked from assignment by an address range
260     if ( !$used{$freeaddr->addr } ) {
261       my ($range) = grep { !$_->allow_use }
262                   FS::addr_range->any_contains($freeaddr->addr);
263       if ( !$range ) {
264         # then we've found a free address
265         return $freeaddr;
266       }
267       # otherwise, skip to the end of the range
268       $freeaddr = NetAddr::IP->new($range->end, $self->ip_netmask);
269     }
270     $freeaddr++;
271   }
272   return;
273
274 }
275
276 =item allocate -- deprecated
277
278 Allocates this address block to a router.  Takes an FS::router object 
279 as an argument.
280
281 At present it's not possible to reallocate a block to a different router 
282 except by deallocating it first, which requires that none of its addresses 
283 be assigned.  This is probably as it should be.
284
285 =cut
286
287 sub allocate {
288   my ($self, $router) = @_;
289   carp "deallocate deprecated -- use replace";
290
291   return 'Block must be allocated to a router'
292     unless(ref $router eq 'FS::router');
293
294   my $new = new FS::addr_block {$self->hash};
295   $new->routernum($router->routernum);
296   return $new->replace($self);
297
298 }
299
300 =item deallocate -- deprecated
301
302 Deallocates the block (i.e. sets the routernum to 0).  If any addresses in the 
303 block are assigned to services, it fails.
304
305 =cut
306
307 sub deallocate {
308   carp "deallocate deprecated -- use replace";
309   my $self = shift;
310
311   my $new = new FS::addr_block {$self->hash};
312   $new->routernum(0);
313   return $new->replace($self);
314 }
315
316 =item split_block
317
318 Splits this address block into two equal blocks, occupying the same space as
319 the original block.  The first of the two will also have the same blocknum.
320 The gateway address of each block will be set to the first usable address, i.e.
321 (network address)+1.  Since this method is designed for use on unallocated
322 blocks, this is probably the correct behavior.
323
324 (At present, splitting allocated blocks is disallowed.  Anyone who wants to
325 implement this is reminded that each split costs three addresses, and any
326 customers who were using these addresses will have to be moved; depending on
327 how full the block was before being split, they might have to be moved to a
328 different block.  Anyone who I<still> wants to implement it is asked to tie it
329 to a configuration switch so that site admins can disallow it.)
330
331 =cut
332
333 sub split_block {
334
335   # We should consider using Attribute::Handlers/Aspect/Hook::LexWrap/
336   # something to atomicize functions, so that we can say 
337   #
338   # sub split_block : atomic {
339   # 
340   # instead of repeating all this AutoCommit verbage in every 
341   # sub that does more than one database operation.
342
343   my $oldAutoCommit = $FS::UID::AutoCommit;
344   local $FS::UID::AutoCommit = 0;
345   my $dbh = dbh;
346
347   my $self = shift;
348   my $error;
349
350   if ($self->router) {
351     return 'Block is already allocated';
352   }
353
354   #TODO: Smallest allowed block should be a config option.
355   if ($self->NetAddr->masklen() ge 30) {
356     return 'Cannot split blocks with a mask length >= 30';
357   }
358
359   my (@new, @ip);
360   $ip[0] = $self->NetAddr;
361   @ip = map {$_->first()} $ip[0]->split($self->ip_netmask + 1);
362
363   foreach (0,1) {
364     $new[$_] = new FS::addr_block {$self->hash};
365     $new[$_]->ip_gateway($ip[$_]->addr);
366     $new[$_]->ip_netmask($ip[$_]->masklen);
367   }
368
369   $new[1]->blocknum('');
370
371   $error = $new[0]->replace($self);
372   if ($error) {
373     $dbh->rollback;
374     return $error;
375   }
376
377   $error = $new[1]->insert;
378   if ($error) {
379     $dbh->rollback;
380     return $error;
381   }
382
383   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
384   return '';
385 }
386
387 =item merge
388
389 To be implemented.
390
391 =item agent
392
393 Returns the agent (see L<FS::agent>) for this address block, if one exists.
394
395 =item label
396
397 Returns text including the router name, gateway ip, and netmask for this
398 block.
399
400 =cut
401
402 sub label {
403   my $self = shift;
404   my $router = $self->router;
405   ($router ? $router->routername : '(unallocated)'). ':'. $self->NetAddr;
406 }
407
408 =item router
409
410 Returns the router assigned to this block.
411
412 =cut
413
414 # necessary, because this can't be foreign keyed
415
416 sub router {
417   my $self = shift;
418   my $routernum = $self->routernum;
419   if ( $routernum ) {
420     return FS::router->by_key($routernum);
421   } else {
422     return;
423   }
424 }
425
426 =back
427
428 =head1 BUGS
429
430 Minimum block size should be a config option.  It's hardcoded at /30 right
431 now because that's the smallest block that makes any sense at all.
432
433 =cut
434
435 1;