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