actually make auto-assignment avoid forbidden ranges, #25530
[freeside.git] / FS / FS / addr_block.pm
1 package FS::addr_block;
2
3 use strict;
4 use vars qw( @ISA );
5 use FS::Record qw( qsearchs qsearch dbh );
6 use FS::router;
7 use FS::svc_broadband;
8 use FS::Conf;
9 use FS::IP_Mixin;
10 use NetAddr::IP;
11 use Carp qw( carp );
12 use List::Util qw( first );
13
14 @ISA = qw( FS::Record );
15
16 =head1 NAME
17
18 FS::addr_block - Object methods for addr_block records
19
20 =head1 SYNOPSIS
21
22   use FS::addr_block;
23
24   $record = new FS::addr_block \%hash;
25   $record = new FS::addr_block { 'column' => 'value' };
26
27   $error = $record->insert;
28
29   $error = $new_record->replace($old_record);
30
31   $error = $record->delete;
32
33   $error = $record->check;
34
35 =head1 DESCRIPTION
36
37 An FS::addr_block record describes an address block assigned for broadband 
38 access.  FS::addr_block inherits from FS::Record.  The following fields are 
39 currently supported:
40
41 =over 4
42
43 =item blocknum - primary key, used in FS::svc_broadband to associate 
44 services to the block.
45
46 =item routernum - the router (see FS::router) to which this 
47 block is assigned.
48
49 =item ip_gateway - the gateway address used by customers within this block.  
50
51 =item ip_netmask - the netmask of the block, expressed as an integer.
52
53 =item manual_flag - prohibit automatic ip assignment from this block when true. 
54
55 =item agentnum - optional agent number (see L<FS::agent>)
56
57 =back
58
59 =head1 METHODS
60
61 =over 4
62
63 =item new HASHREF
64
65 Create a new record.  To add the record to the database, see "insert".
66
67 =cut
68
69 sub table { 'addr_block'; }
70
71 =item insert
72
73 Adds this record to the database.  If there is an error, returns the error,
74 otherwise returns false.
75
76 =item delete
77
78 Deletes this record from the database.  If there is an error, returns the
79 error, otherwise returns false.
80
81 =cut
82
83 sub delete {
84   my $self = shift;
85   return 'Block must be deallocated and have no services before deletion'
86     if $self->router || $self->svc_broadband;
87
88     local $SIG{HUP} = 'IGNORE';
89     local $SIG{INT} = 'IGNORE';
90     local $SIG{QUIT} = 'IGNORE';
91     local $SIG{TERM} = 'IGNORE';
92     local $SIG{TSTP} = 'IGNORE';
93     local $SIG{PIPE} = 'IGNORE';
94
95     my $oldAutoCommit = $FS::UID::AutoCommit;
96     local $FS::UID::AutoCommit = 0;
97     my $dbh = dbh;
98     
99     my $error = $self->SUPER::delete;
100     if ( $error ) {
101        $dbh->rollback if $oldAutoCommit;
102        return $error;
103     }
104   
105     $dbh->commit or die $dbh->errstr if $oldAutoCommit;
106     '';
107 }
108
109
110 =item replace OLD_RECORD
111
112 Replaces OLD_RECORD with this one in the database.  If there is an error,
113 returns the error, otherwise returns false.
114
115 At present it's not possible to reallocate a block to a different router 
116 except by deallocating it first, which requires that none of its addresses 
117 be assigned.  This is probably as it should be.
118
119 sub replace_check {
120   my ( $new, $old ) = ( shift, shift );
121
122   unless($new->routernum == $old->routernum) {
123     my @svc = $self->svc_broadband;
124     if (@svc) {
125       return 'Block has assigned addresses: '.
126              join ', ', map {$_->ip_addr} @svc;
127     }
128
129     return 'Block is already allocated'
130       if($new->routernum && $old->routernum);
131
132   }
133
134   '';
135 }
136
137 =item check
138
139 Checks all fields to make sure this is a valid record.  If there is an error,
140 returns the error, otherwise returns false.  Called by the insert and replace
141 methods.
142
143 =cut
144
145 sub check {
146   my $self = shift;
147
148   my $error =
149     $self->ut_number('routernum')
150     || $self->ut_ip('ip_gateway')
151     || $self->ut_number('ip_netmask')
152     || $self->ut_enum('manual_flag', [ '', 'Y' ])
153     || $self->ut_agentnum_acl('agentnum', 'Broadband global configuration')
154   ;
155   return $error if $error;
156
157
158   # A routernum of 0 indicates an unassigned block and is allowed
159   return "Unknown routernum"
160     if ($self->routernum and not $self->router);
161
162   my $self_addr = $self->NetAddr;
163   return "Cannot parse address: ". $self->ip_gateway . '/' . $self->ip_netmask
164     unless $self_addr;
165
166   if (not $self->blocknum) {
167     my @block = grep {
168       my $block_addr = $_->NetAddr;
169       if($block_addr->contains($self_addr) 
170       or $self_addr->contains($block_addr)) { $_; };
171     } qsearch( 'addr_block', {});
172     foreach(@block) {
173       return "Block intersects existing block ".$_->ip_gateway."/".$_->ip_netmask;
174     }
175   }
176
177   $self->SUPER::check;
178 }
179
180
181 =item router
182
183 Returns the FS::router object corresponding to this object.  If the 
184 block is unassigned, returns undef.
185
186 =cut
187
188 sub router {
189   my $self = shift;
190   return qsearchs('router', { routernum => $self->routernum });
191 }
192
193 =item svc_broadband
194
195 Returns a list of FS::svc_broadband objects associated
196 with this object.
197
198 =cut
199
200 sub svc_broadband {
201   my $self = shift;
202   return qsearch('svc_broadband', { blocknum => $self->blocknum });
203 }
204
205 =item NetAddr
206
207 Returns a NetAddr::IP object for this block's address and netmask.
208
209 =cut
210
211 sub NetAddr {
212   my $self = shift;
213   new NetAddr::IP ($self->ip_gateway, $self->ip_netmask);
214 }
215
216 =item cidr
217
218 Returns a CIDR string for this block's address and netmask, i.e. 10.4.20.0/24
219
220 =cut
221
222 sub cidr {
223   my $self = shift;
224   $self->NetAddr->cidr;
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($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);
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 =cut
396
397 sub agent {
398   qsearchs('agent', { 'agentnum' => shift->agentnum } );
399 }
400
401 =item label
402
403 Returns text including the router name, gateway ip, and netmask for this
404 block.
405
406 =cut
407
408 sub label {
409   my $self = shift;
410   my $router = $self->router;
411   ($router ? $router->routername : '(unallocated)'). ':'. $self->NetAddr;
412 }
413
414 =back
415
416 =head1 BUGS
417
418 Minimum block size should be a config option.  It's hardcoded at /30 right
419 now because that's the smallest block that makes any sense at all.
420
421 =cut
422
423 1;
424