allow null svc_broadband.ip_addr
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2
3 use strict;
4 use vars qw(@ISA $conf);
5 use FS::Record qw( qsearchs qsearch dbh );
6 use FS::svc_Common;
7 use FS::cust_svc;
8 use FS::addr_block;
9 use FS::part_svc_router;
10 use NetAddr::IP;
11
12 @ISA = qw( FS::svc_Common );
13
14 $FS::UID::callback{'FS::svc_broadband'} = sub { 
15   $conf = new FS::Conf;
16 };
17
18 =head1 NAME
19
20 FS::svc_broadband - Object methods for svc_broadband records
21
22 =head1 SYNOPSIS
23
24   use FS::svc_broadband;
25
26   $record = new FS::svc_broadband \%hash;
27   $record = new FS::svc_broadband { 'column' => 'value' };
28
29   $error = $record->insert;
30
31   $error = $new_record->replace($old_record);
32
33   $error = $record->delete;
34
35   $error = $record->check;
36
37   $error = $record->suspend;
38
39   $error = $record->unsuspend;
40
41   $error = $record->cancel;
42
43 =head1 DESCRIPTION
44
45 An FS::svc_broadband object represents a 'broadband' Internet connection, such
46 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
47 have the following properties:
48
49 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
50 currently supported:
51
52 =over 4
53
54 =item svcnum - primary key
55
56 =item blocknum - see FS::addr_block
57
58 =item
59 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
60 speed will be unlimited.  Exports that do traffic shaping should handle this
61 correctly, and not blindly set the upload speed to zero and kill the customer's
62 connection.
63
64 =item
65 speed_down - maximum download speed, as above
66
67 =item ip_addr - the customer's IP address.  If the customer needs more than one
68 IP address, set this to the address of the customer's router.  As a result, the
69 customer's router will have the same address for both its internal and external
70 interfaces thus saving address space.  This has been found to work on most NAT
71 routers available.
72
73 =back
74
75 =head1 METHODS
76
77 =over 4
78
79 =item new HASHREF
80
81 Creates a new svc_broadband.  To add the record to the database, see
82 "insert".
83
84 Note that this stores the hash reference, not a distinct copy of the hash it
85 points to.  You can ask the object for a copy with the I<hash> method.
86
87 =cut
88
89 sub table_info {
90   {
91     'name' => 'Broadband',
92     'name_plural' => 'Broadband services',
93     'longname_plural' => 'Fixed (username-less) broadband services',
94     'display_weight' => 50,
95     'cancel_weight'  => 70,
96     'fields' => {
97       'description' => 'Descriptive label for this particular device.',
98       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
99       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
100       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
101       'blocknum'    => { 'label' => 'Address block',
102                          'type'  => 'select',
103                          'select_table' => 'addr_block',
104                          'select_key'   => 'blocknum',
105                          'select_label' => 'cidr',
106                          'disable_inventory' => 1,
107                        },
108     },
109   };
110 }
111
112 sub table { 'svc_broadband'; }
113
114 sub table_dupcheck_fields { ( 'mac_addr' ); }
115
116 =item search_sql STRING
117
118 Class method which returns an SQL fragment to search for the given string.
119
120 =cut
121
122 sub search_sql {
123   my( $class, $string ) = @_;
124   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
125     $class->search_sql_field('ip_addr', $string );
126   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
127     $class->search_sql_field('mac_addr', uc($string));
128   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
129     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
130   } else {
131     '1 = 0'; #false
132   }
133 }
134
135 =item label
136
137 Returns the IP address.
138
139 =cut
140
141 sub label {
142   my $self = shift;
143   $self->ip_addr;
144 }
145
146 =item insert [ , OPTION => VALUE ... ]
147
148 Adds this record to the database.  If there is an error, returns the error,
149 otherwise returns false.
150
151 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
152 defined.  An FS::cust_svc record will be created and inserted.
153
154 Currently available options are: I<depend_jobnum>
155
156 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
157 jobnums), all provisioning jobs will have a dependancy on the supplied
158 jobnum(s) (they will not run until the specific job(s) complete(s)).
159
160 =cut
161
162 # Standard FS::svc_Common::insert
163
164 =item delete
165
166 Delete this record from the database.
167
168 =cut
169
170 # Standard FS::svc_Common::delete
171
172 =item replace OLD_RECORD
173
174 Replaces the OLD_RECORD with this one in the database.  If there is an error,
175 returns the error, otherwise returns false.
176
177 =cut
178
179 # Standard FS::svc_Common::replace
180
181 =item suspend
182
183 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
184
185 =item unsuspend
186
187 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
188
189 =item cancel
190
191 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
192
193 =item check
194
195 Checks all fields to make sure this is a valid broadband service.  If there is
196 an error, returns the error, otherwise returns false.  Called by the insert
197 and replace methods.
198
199 =cut
200
201 sub check {
202   my $self = shift;
203   my $x = $self->setfixed;
204
205   return $x unless ref($x);
206
207   my $error =
208     $self->ut_numbern('svcnum')
209     || $self->ut_numbern('blocknum')
210     || $self->ut_textn('description')
211     || $self->ut_number('speed_up')
212     || $self->ut_number('speed_down')
213     || $self->ut_ipn('ip_addr')
214     || $self->ut_hexn('mac_addr')
215     || $self->ut_hexn('auth_key')
216     || $self->ut_coordn('latitude', -90, 90)
217     || $self->ut_coordn('longitude', -180, 180)
218     || $self->ut_sfloatn('altitude')
219     || $self->ut_textn('vlan_profile')
220   ;
221   return $error if $error;
222
223   if($self->speed_up < 0) { return 'speed_up must be positive'; }
224   if($self->speed_down < 0) { return 'speed_down must be positive'; }
225
226   my $cust_svc = $self->svcnum
227                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
228                  : '';
229   my $cust_pkg;
230   if ($cust_svc) {
231     $cust_pkg = $cust_svc->cust_pkg;
232   }else{
233     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
234     return "Invalid pkgnum" unless $cust_pkg;
235   }
236     
237   if ($self->blocknum) {
238     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
239     return $error if $error;
240   }
241
242   if ($cust_pkg && $self->blocknum) {
243     my $addr_agentnum = $self->addr_block->agentnum;
244     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
245       return "Address block does not service this customer";
246     }
247   }
248
249   $error = $self->_check_ip_addr;
250   return $error if $error;
251
252   $self->SUPER::check;
253 }
254
255 sub _check_ip_addr {
256   my $self = shift;
257
258   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
259
260     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
261
262     return "Must supply either address or block"
263       unless $self->blocknum;
264     my $next_addr = $self->addr_block->next_free_addr;
265     if ($next_addr) {
266       $self->ip_addr($next_addr->addr);
267     } else {
268       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
269     }
270
271   }
272
273   if (not($self->blocknum)) {
274     return "Must supply either address or block"
275       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
276     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
277                  map { $_->addr_block }
278                  $self->allowed_routers;
279     if (scalar(@block)) {
280       $self->blocknum($block[0]->blocknum);
281     }else{
282       return "Address not with available block.";
283     }
284   }
285
286   # This should catch errors in the ip_addr.  If it doesn't,
287   # they'll almost certainly not map into the block anyway.
288   my $self_addr = $self->NetAddr; #netmask is /32
289   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
290
291   my $block_addr = $self->addr_block->NetAddr;
292   unless ($block_addr->contains($self_addr)) {
293     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
294   }
295
296   my $router = $self->addr_block->router 
297     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
298   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
299   } # do nothing
300   else {
301     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
302   }
303
304   '';
305 }
306
307 sub _check_duplicate {
308   my $self = shift;
309
310   return "MAC already in use"
311     if ( $self->mac_addr &&
312          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
313        );
314
315   '';
316 }
317
318
319 =item NetAddr
320
321 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
322 is /32.
323
324 =cut
325
326 sub NetAddr {
327   my $self = shift;
328   new NetAddr::IP ($self->ip_addr);
329 }
330
331 =item addr_block
332
333 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
334
335 =cut
336
337 sub addr_block {
338   my $self = shift;
339   qsearchs('addr_block', { blocknum => $self->blocknum });
340 }
341
342 =back
343
344 =item allowed_routers
345
346 Returns a list of allowed FS::router objects.
347
348 =cut
349
350 sub allowed_routers {
351   my $self = shift;
352   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
353 }
354
355 =head1 BUGS
356
357 The business with sb_field has been 'fixed', in a manner of speaking.
358
359 allowed_routers isn't agent virtualized because part_svc isn't agent
360 virtualized
361
362 =head1 SEE ALSO
363
364 FS::svc_Common, FS::Record, FS::addr_block,
365 FS::part_svc, schema.html from the base documentation.
366
367 =cut
368
369 1;
370