2ac24e55b7b36c5778279c761534017ba864d59a
[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_Radius_Mixin 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 =item plan_id
74
75 =back
76
77 =head1 METHODS
78
79 =over 4
80
81 =item new HASHREF
82
83 Creates a new svc_broadband.  To add the record to the database, see
84 "insert".
85
86 Note that this stores the hash reference, not a distinct copy of the hash it
87 points to.  You can ask the object for a copy with the I<hash> method.
88
89 =cut
90
91 sub table_info {
92   {
93     'name' => 'Broadband',
94     'name_plural' => 'Broadband services',
95     'longname_plural' => 'Fixed (username-less) broadband services',
96     'display_weight' => 50,
97     'cancel_weight'  => 70,
98     'fields' => {
99       'description' => 'Descriptive label for this particular device.',
100       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
101       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
102       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
103       'blocknum'    => { 'label' => 'Address block',
104                          'type'  => 'select',
105                          'select_table' => 'addr_block',
106                          'select_key'   => 'blocknum',
107                          'select_label' => 'cidr',
108                          'disable_inventory' => 1,
109                        },
110      'plan_id' => 'Service Plan Id',
111      'performance_profile' => 'Peformance Profile',
112      'authkey'      => 'Authentication key',
113      'mac_addr'     => 'MAC address',
114      'latitude'     => 'Latitude',
115      'longitude'    => 'Longitude',
116      'altitude'     => 'Altitude',
117      'vlan_profile' => 'VLAN profile',
118      'usergroup'    => { 
119                          label => 'RADIUS groups',
120                          type  => 'select-radius_group.html',
121                          #select_table => 'radius_group',
122                          #select_key   => 'groupnum',
123                          #select_label => 'groupname',
124                          disable_inventory => 1,
125                          multiple => 1,
126                        },
127     },
128   };
129 }
130
131 sub table { 'svc_broadband'; }
132
133 sub table_dupcheck_fields { ( 'mac_addr' ); }
134
135 =item search HASHREF
136
137 Class method which returns a qsearch hash expression to search for parameters
138 specified in HASHREF.
139
140 Parameters:
141
142 =over 4
143
144 =item unlinked - set to search for all unlinked services.  Overrides all other options.
145
146 =item agentnum
147
148 =item custnum
149
150 =item svcpart
151
152 =item ip_addr
153
154 =item pkgpart - arrayref
155
156 =item routernum - arrayref
157
158 =item order_by
159
160 =back
161
162 =cut
163
164 sub search {
165   my ($class, $params) = @_;
166   my @where = ();
167   my @from = (
168     'LEFT JOIN cust_svc  USING ( svcnum  )',
169     'LEFT JOIN part_svc  USING ( svcpart )',
170     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
171     'LEFT JOIN cust_main USING ( custnum )',
172   );
173
174   # based on FS::svc_acct::search, probably the most mature of the bunch
175   #unlinked
176   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
177   
178   #agentnum
179   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
180     push @where, "cust_main.agentnum = $1";
181   }
182   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
183     'null_right' => 'View/link unlinked services',
184     'table' => 'cust_main'
185   );
186
187   #custnum
188   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
189     push @where, "custnum = $1";
190   }
191
192   #pkgpart, now properly untainted, can be arrayref
193   for my $pkgpart ( $params->{'pkgpart'} ) {
194     if ( ref $pkgpart ) {
195       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
196       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
197     }
198     elsif ( $pkgpart =~ /^(\d+)$/ ) {
199       push @where, "cust_pkg.pkgpart = $1";
200     }
201   }
202
203   #routernum, can be arrayref
204   for my $routernum ( $params->{'routernum'} ) {
205     push @from, 'LEFT JOIN addr_block USING ( blocknum )';
206     if ( ref $routernum and grep { $_ } @$routernum ) {
207       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
208       push @where, "addr_block.routernum IN ($where)" if $where;
209     }
210     elsif ( $routernum =~ /^(\d+)$/ ) {
211       push @where, "addr_block.routernum = $1";
212     }
213   }
214  
215   #svcnum
216   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
217     push @where, "svcnum = $1";
218   }
219
220   #svcpart
221   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
222     push @where, "svcpart = $1";
223   }
224
225   #ip_addr
226   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
227     push @where, "ip_addr = '$1'";
228   }
229
230   #custnum
231   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
232     push @where, "custnum = $1";
233   }
234   
235   my $addl_from = join(' ', @from);
236   my $extra_sql = '';
237   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
238   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
239   return( {
240       'table'   => 'svc_broadband',
241       'hashref' => {},
242       'select'  => join(', ',
243         'svc_broadband.*',
244         'part_svc.svc',
245         'cust_main.custnum',
246         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
247       ),
248       'extra_sql' => $extra_sql,
249       'addl_from' => $addl_from,
250       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
251       'count_query' => $count_query,
252     } );
253 }
254
255 =item search_sql STRING
256
257 Class method which returns an SQL fragment to search for the given string.
258
259 =cut
260
261 sub search_sql {
262   my( $class, $string ) = @_;
263   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
264     $class->search_sql_field('ip_addr', $string );
265   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
266     $class->search_sql_field('mac_addr', uc($string));
267   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
268     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
269   } else {
270     '1 = 0'; #false
271   }
272 }
273
274 =item label
275
276 Returns the IP address.
277
278 =cut
279
280 sub label {
281   my $self = shift;
282   $self->ip_addr;
283 }
284
285 =item insert [ , OPTION => VALUE ... ]
286
287 Adds this record to the database.  If there is an error, returns the error,
288 otherwise returns false.
289
290 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
291 defined.  An FS::cust_svc record will be created and inserted.
292
293 Currently available options are: I<depend_jobnum>
294
295 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
296 jobnums), all provisioning jobs will have a dependancy on the supplied
297 jobnum(s) (they will not run until the specific job(s) complete(s)).
298
299 =cut
300
301 # Standard FS::svc_Common::insert
302
303 =item delete
304
305 Delete this record from the database.
306
307 =cut
308
309 # Standard FS::svc_Common::delete
310
311 =item replace OLD_RECORD
312
313 Replaces the OLD_RECORD with this one in the database.  If there is an error,
314 returns the error, otherwise returns false.
315
316 =cut
317
318 # Standard FS::svc_Common::replace
319
320 =item suspend
321
322 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
323
324 =item unsuspend
325
326 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
327
328 =item cancel
329
330 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
331
332 =item check
333
334 Checks all fields to make sure this is a valid broadband service.  If there is
335 an error, returns the error, otherwise returns false.  Called by the insert
336 and replace methods.
337
338 =cut
339
340 sub check {
341   my $self = shift;
342   my $x = $self->setfixed;
343
344   return $x unless ref($x);
345
346   my $nw_coords = $conf->exists('svc_broadband-require-nw-coordinates');
347   my $lat_lower = $nw_coords ? 1 : -90;
348   my $lon_upper = $nw_coords ? -1 : 180;
349
350   # remove delimiters
351   my $mac_addr = uc($self->get('mac_addr'));
352   $mac_addr =~ s/[-: ]//g;
353   $self->set('mac_addr', $mac_addr);
354
355   my $error =
356     $self->ut_numbern('svcnum')
357     || $self->ut_numbern('blocknum')
358     || $self->ut_textn('description')
359     || $self->ut_numbern('speed_up')
360     || $self->ut_numbern('speed_down')
361     || $self->ut_ipn('ip_addr')
362     || $self->ut_hexn('mac_addr')
363     || $self->ut_hexn('auth_key')
364     || $self->ut_coordn('latitude', $lat_lower, 90)
365     || $self->ut_coordn('longitude', -180, $lon_upper)
366     || $self->ut_sfloatn('altitude')
367     || $self->ut_textn('vlan_profile')
368     || $self->ut_textn('plan_id')
369   ;
370   return $error if $error;
371
372   if($self->speed_up < 0) { return 'speed_up must be positive'; }
373   if($self->speed_down < 0) { return 'speed_down must be positive'; }
374
375   my $cust_svc = $self->svcnum
376                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
377                  : '';
378   my $cust_pkg;
379   if ($cust_svc) {
380     $cust_pkg = $cust_svc->cust_pkg;
381   }else{
382     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
383     return "Invalid pkgnum" unless $cust_pkg;
384   }
385     
386   if ($self->blocknum) {
387     $error = $self->ut_foreign_key('blocknum', 'addr_block', 'blocknum');
388     return $error if $error;
389   }
390
391   if ($cust_pkg && $self->blocknum) {
392     my $addr_agentnum = $self->addr_block->agentnum;
393     if ($addr_agentnum && $addr_agentnum != $cust_pkg->cust_main->agentnum) {
394       return "Address block does not service this customer";
395     }
396   }
397
398   $error = $self->_check_ip_addr;
399   return $error if $error;
400
401   $self->SUPER::check;
402 }
403
404 sub _check_ip_addr {
405   my $self = shift;
406
407   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
408
409     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); #&& !$self->blocknum
410
411     return "Must supply either address or block"
412       unless $self->blocknum;
413     my $next_addr = $self->addr_block->next_free_addr;
414     if ($next_addr) {
415       $self->ip_addr($next_addr->addr);
416     } else {
417       return "No free addresses in addr_block (blocknum: ".$self->blocknum.")";
418     }
419
420   }
421
422   if (not($self->blocknum)) {
423     return "Must supply either address or block"
424       unless ($self->ip_addr and $self->ip_addr ne '0.0.0.0');
425     my @block = grep { $_->NetAddr->contains($self->NetAddr) }
426                  map { $_->addr_block }
427                  $self->allowed_routers;
428     if (scalar(@block)) {
429       $self->blocknum($block[0]->blocknum);
430     }else{
431       return "Address not with available block.";
432     }
433   }
434
435   # This should catch errors in the ip_addr.  If it doesn't,
436   # they'll almost certainly not map into the block anyway.
437   my $self_addr = $self->NetAddr; #netmask is /32
438   return ('Cannot parse address: ' . $self->ip_addr) unless $self_addr;
439
440   my $block_addr = $self->addr_block->NetAddr;
441   unless ($block_addr->contains($self_addr)) {
442     return 'blocknum '.$self->blocknum.' does not contain address '.$self->ip_addr;
443   }
444
445   my $router = $self->addr_block->router 
446     or return 'Cannot assign address from unallocated block:'.$self->addr_block->blocknum;
447   if(grep { $_->routernum == $router->routernum} $self->allowed_routers) {
448   } # do nothing
449   else {
450     return 'Router '.$router->routernum.' cannot provide svcpart '.$self->svcpart;
451   }
452
453   '';
454 }
455
456 sub _check_duplicate {
457   my $self = shift;
458
459   return "MAC already in use"
460     if ( $self->mac_addr &&
461          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
462        );
463
464   '';
465 }
466
467
468 =item NetAddr
469
470 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
471 is /32.
472
473 =cut
474
475 sub NetAddr {
476   my $self = shift;
477   new NetAddr::IP ($self->ip_addr);
478 }
479
480 =item addr_block
481
482 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
483
484 =cut
485
486 sub addr_block {
487   my $self = shift;
488   qsearchs('addr_block', { blocknum => $self->blocknum });
489 }
490
491 =back
492
493 =item allowed_routers
494
495 Returns a list of allowed FS::router objects.
496
497 =cut
498
499 sub allowed_routers {
500   my $self = shift;
501   map { $_->router } qsearch('part_svc_router', { svcpart => $self->svcpart });
502 }
503
504 =head1 BUGS
505
506 The business with sb_field has been 'fixed', in a manner of speaking.
507
508 allowed_routers isn't agent virtualized because part_svc isn't agent
509 virtualized
510
511 =head1 SEE ALSO
512
513 FS::svc_Common, FS::Record, FS::addr_block,
514 FS::part_svc, schema.html from the base documentation.
515
516 =cut
517
518 1;
519