5b69127175f104a02edd9c2e9d205869621c0f67
[freeside.git] / FS / FS / svc_broadband.pm
1 package FS::svc_broadband;
2
3 use strict;
4 use vars qw(@ISA $conf);
5
6 use base qw(FS::svc_Radius_Mixin FS::svc_Tower_Mixin FS::svc_Common);
7 { no warnings 'redefine'; use NetAddr::IP; }
8 use FS::Record qw( qsearchs qsearch dbh );
9 use FS::svc_Common;
10 use FS::cust_svc;
11 use FS::addr_block;
12 use FS::part_svc_router;
13 use FS::tower_sector;
14
15 $FS::UID::callback{'FS::svc_broadband'} = sub { 
16   $conf = new FS::Conf;
17 };
18
19 =head1 NAME
20
21 FS::svc_broadband - Object methods for svc_broadband records
22
23 =head1 SYNOPSIS
24
25   use FS::svc_broadband;
26
27   $record = new FS::svc_broadband \%hash;
28   $record = new FS::svc_broadband { 'column' => 'value' };
29
30   $error = $record->insert;
31
32   $error = $new_record->replace($old_record);
33
34   $error = $record->delete;
35
36   $error = $record->check;
37
38   $error = $record->suspend;
39
40   $error = $record->unsuspend;
41
42   $error = $record->cancel;
43
44 =head1 DESCRIPTION
45
46 An FS::svc_broadband object represents a 'broadband' Internet connection, such
47 as a DSL, cable modem, or fixed wireless link.  These services are assumed to
48 have the following properties:
49
50 FS::svc_broadband inherits from FS::svc_Common.  The following fields are
51 currently supported:
52
53 =over 4
54
55 =item svcnum - primary key
56
57 =item blocknum - see FS::addr_block
58
59 =item
60 speed_up - maximum upload speed, in bits per second.  If set to zero, upload
61 speed will be unlimited.  Exports that do traffic shaping should handle this
62 correctly, and not blindly set the upload speed to zero and kill the customer's
63 connection.
64
65 =item
66 speed_down - maximum download speed, as above
67
68 =item ip_addr - the customer's IP address.  If the customer needs more than one
69 IP address, set this to the address of the customer's router.  As a result, the
70 customer's router will have the same address for both its internal and external
71 interfaces thus saving address space.  This has been found to work on most NAT
72 routers available.
73
74 =item plan_id
75
76 =back
77
78 =head1 METHODS
79
80 =over 4
81
82 =item new HASHREF
83
84 Creates a new svc_broadband.  To add the record to the database, see
85 "insert".
86
87 Note that this stores the hash reference, not a distinct copy of the hash it
88 points to.  You can ask the object for a copy with the I<hash> method.
89
90 =cut
91
92 sub table_info {
93   {
94     'name' => 'Wireless broadband',
95     'name_plural' => 'Wireless broadband services',
96     'longname_plural' => 'Fixed wireless broadband services',
97     'display_weight' => 50,
98     'cancel_weight'  => 70,
99     'ip_field' => 'ip_addr',
100     'fields' => {
101       'svcnum'      => 'Service',
102       'description' => 'Descriptive label for this particular device',
103       'speed_down'  => 'Maximum download speed for this service in Kbps.  0 denotes unlimited.',
104       'speed_up'    => 'Maximum upload speed for this service in Kbps.  0 denotes unlimited.',
105       'ip_addr'     => 'IP address.  Leave blank for automatic assignment.',
106       'blocknum'    => 
107       { 'label' => 'Address block',
108                          'type'  => 'select',
109                          'select_table' => 'addr_block',
110                           'select_key'   => 'blocknum',
111                          'select_label' => 'cidr',
112                          'disable_inventory' => 1,
113                        },
114      'plan_id' => 'Service Plan Id',
115      'performance_profile' => 'Peformance Profile',
116      'authkey'      => 'Authentication key',
117      'mac_addr'     => 'MAC address',
118      'latitude'     => 'Latitude',
119      'longitude'    => 'Longitude',
120      'altitude'     => 'Altitude',
121      'vlan_profile' => 'VLAN profile',
122      'sectornum'    => 'Tower/sector',
123      'routernum'    => 'Router/block',
124      'usergroup'    => { 
125                          label => 'RADIUS groups',
126                          type  => 'select-radius_group.html',
127                          #select_table => 'radius_group',
128                          #select_key   => 'groupnum',
129                          #select_label => 'groupname',
130                          disable_inventory => 1,
131                          multiple => 1,
132                        },
133     },
134   };
135 }
136
137 sub table { 'svc_broadband'; }
138
139 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
140
141 =item search HASHREF
142
143 Class method which returns a qsearch hash expression to search for parameters
144 specified in HASHREF.
145
146 Parameters:
147
148 =over 4
149
150 =item unlinked - set to search for all unlinked services.  Overrides all other options.
151
152 =item agentnum
153
154 =item custnum
155
156 =item svcpart
157
158 =item ip_addr
159
160 =item pkgpart - arrayref
161
162 =item routernum - arrayref
163
164 =item sectornum - arrayref
165
166 =item towernum - arrayref
167
168 =item order_by
169
170 =back
171
172 =cut
173
174 sub search {
175   my ($class, $params) = @_;
176   my @where = ();
177   my @from = (
178     'LEFT JOIN cust_svc  USING ( svcnum  )',
179     'LEFT JOIN part_svc  USING ( svcpart )',
180     'LEFT JOIN cust_pkg  USING ( pkgnum  )',
181     'LEFT JOIN cust_main USING ( custnum )',
182   );
183
184   # based on FS::svc_acct::search, probably the most mature of the bunch
185   #unlinked
186   push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
187   
188   #agentnum
189   if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
190     push @where, "cust_main.agentnum = $1";
191   }
192   push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
193     'null_right' => 'View/link unlinked services',
194     'table' => 'cust_main'
195   );
196
197   #custnum
198   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
199     push @where, "custnum = $1";
200   }
201
202   #pkgpart, now properly untainted, can be arrayref
203   for my $pkgpart ( $params->{'pkgpart'} ) {
204     if ( ref $pkgpart ) {
205       my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
206       push @where, "cust_pkg.pkgpart IN ($where)" if $where;
207     }
208     elsif ( $pkgpart =~ /^(\d+)$/ ) {
209       push @where, "cust_pkg.pkgpart = $1";
210     }
211   }
212
213   #routernum, can be arrayref
214   for my $routernum ( $params->{'routernum'} ) {
215     # this no longer uses addr_block
216     if ( ref $routernum and grep { $_ } @$routernum ) {
217       my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
218       my @orwhere;
219       push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
220       push @orwhere, "svc_broadband.routernum IS NULL" 
221         if grep /^none$/, @$routernum;
222       push @where, '( '.join(' OR ', @orwhere).' )';
223     }
224     elsif ( $routernum =~ /^(\d+)$/ ) {
225       push @where, "svc_broadband.routernum = $1";
226     }
227     elsif ( $routernum eq 'none' ) {
228       push @where, "svc_broadband.routernum IS NULL";
229     }
230   }
231
232   #sector and tower, as above
233   my @where_sector = $class->tower_sector_sql($params);
234   if ( @where_sector ) {
235     push @where, @where_sector;
236     push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
237   }
238  
239   #svcnum
240   if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
241     push @where, "svcnum = $1";
242   }
243
244   #svcpart
245   if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
246     push @where, "svcpart = $1";
247   }
248
249   #exportnum
250   if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
251     push @from, 'LEFT JOIN export_svc USING ( svcpart )';
252     push @where, "exportnum = $1";
253   }
254
255   #ip_addr
256   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
257     push @where, "ip_addr = '$1'";
258   }
259
260   #custnum
261   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
262     push @where, "custnum = $1";
263   }
264   
265   my $addl_from = join(' ', @from);
266   my $extra_sql = '';
267   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
268   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
269   return( {
270       'table'   => 'svc_broadband',
271       'hashref' => {},
272       'select'  => join(', ',
273         'svc_broadband.*',
274         'part_svc.svc',
275         'cust_main.custnum',
276         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
277       ),
278       'extra_sql' => $extra_sql,
279       'addl_from' => $addl_from,
280       'order_by'  => ($params->{'order_by'} || 'ORDER BY svcnum'),
281       'count_query' => $count_query,
282     } );
283 }
284
285 =item search_sql STRING
286
287 Class method which returns an SQL fragment to search for the given string.
288
289 =cut
290
291 sub search_sql {
292   my( $class, $string ) = @_;
293   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
294     $class->search_sql_field('ip_addr', $string );
295   } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
296     $class->search_sql_field('mac_addr', uc($string));
297   } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
298     $string =~ s/://g;
299     $class->search_sql_field('mac_addr', uc($string) );
300   } elsif ( $string =~ /^(\d+)$/ ) {
301     my $table = $class->table;
302     "$table.svcnum = $1";
303   } else {
304     '1 = 0'; #false
305   }
306 }
307
308 =item label
309
310 Returns the IP address.
311
312 =cut
313
314 sub label {
315   my $self = shift;
316   my $label = 'IP:'. ($self->ip_addr || 'Unknown');
317   $label .= ', MAC:'. $self->mac_addr
318     if $self->mac_addr;
319   $label .= ' ('. $self->description. ')'
320     if $self->description;
321   return $label;
322 }
323
324 =item insert [ , OPTION => VALUE ... ]
325
326 Adds this record to the database.  If there is an error, returns the error,
327 otherwise returns false.
328
329 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
330 defined.  An FS::cust_svc record will be created and inserted.
331
332 Currently available options are: I<depend_jobnum>
333
334 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
335 jobnums), all provisioning jobs will have a dependancy on the supplied
336 jobnum(s) (they will not run until the specific job(s) complete(s)).
337
338 # Standard FS::svc_Common::insert
339
340 =item delete
341
342 Delete this record from the database.
343
344 =cut
345
346 # Standard FS::svc_Common::delete
347
348 =item replace OLD_RECORD
349
350 Replaces the OLD_RECORD with this one in the database.  If there is an error,
351 returns the error, otherwise returns false.
352
353 # Standard FS::svc_Common::replace
354
355 =item suspend
356
357 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
358
359 =item unsuspend
360
361 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
362
363 =item cancel
364
365 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
366
367 =item check
368
369 Checks all fields to make sure this is a valid broadband service.  If there is
370 an error, returns the error, otherwise returns false.  Called by the insert
371 and replace methods.
372
373 =cut
374
375 sub check {
376   my $self = shift;
377   my $x = $self->setfixed;
378
379   return $x unless ref($x);
380
381   # remove delimiters
382   my $mac_addr = uc($self->get('mac_addr'));
383   $mac_addr =~ s/[\W_]//g;
384   $self->set('mac_addr', $mac_addr);
385
386   my $error =
387     $self->ut_numbern('svcnum')
388     || $self->ut_numbern('blocknum')
389     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
390     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
391     || $self->ut_textn('description')
392     || $self->ut_numbern('speed_up')
393     || $self->ut_numbern('speed_down')
394     || $self->ut_ipn('ip_addr')
395     || $self->ut_hexn('mac_addr')
396     || $self->ut_hexn('auth_key')
397     || $self->ut_coordn('latitude')
398     || $self->ut_coordn('longitude')
399     || $self->ut_sfloatn('altitude')
400     || $self->ut_textn('vlan_profile')
401     || $self->ut_textn('plan_id')
402   ;
403   return $error if $error;
404
405   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
406   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
407
408   my $cust_svc = $self->svcnum
409                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
410                  : '';
411   my $cust_pkg;
412   my $svcpart;
413   if ($cust_svc) {
414     $cust_pkg = $cust_svc->cust_pkg;
415     $svcpart = $cust_svc->svcpart;
416   }else{
417     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
418     return "Invalid pkgnum" unless $cust_pkg;
419     $svcpart = $self->svcpart;
420   }
421   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
422
423   if ( $conf->exists('auto_router') and $self->ip_addr and !$self->routernum ) {
424     # assign_router is guaranteed to provide a router that's legal
425     # for this agent and svcpart
426     my $error = $self->_check_ip_addr || $self->assign_router;
427     return $error if $error;
428   }
429   elsif ($self->routernum) {
430     return "Router ".$self->routernum." does not provide this service"
431       unless qsearchs('part_svc_router', { 
432         svcpart => $svcpart,
433         routernum => $self->routernum
434     });
435   
436     my $router = $self->router;
437     return "Router ".$self->routernum." does not serve this customer"
438       if $router->agentnum and $agentnum and $router->agentnum != $agentnum;
439
440     if ( $router->manual_addr ) {
441       $self->blocknum('');
442     }
443     else {
444       my $addr_block = $self->addr_block;
445       if ( $self->ip_addr eq '' 
446            and not ( $addr_block and $addr_block->manual_flag ) ) {
447         my $error = $self->assign_ip_addr;
448         return $error if $error;
449       }
450     }
451  
452     my $error = $self->_check_ip_addr;
453     return $error if $error;
454   } # if $self->routernum
455
456   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
457     my $l = $cust_pkg->cust_location_or_main;
458     if ( $l->ship_latitude && $l->ship_longitude ) {
459       $self->latitude(  $l->ship_latitude  );
460       $self->longitude( $l->ship_longitude );
461     } elsif ( $l->latitude && $l->longitude ) {
462       $self->latitude(  $l->latitude  );
463       $self->longitude( $l->longitude );
464     }
465   }
466
467   $self->SUPER::check;
468 }
469
470 =item assign_ip_addr
471
472 Assign an IP address matching the selected router, and the selected block
473 if there is one.
474
475 =cut
476
477 sub assign_ip_addr {
478   my $self = shift;
479   my @blocks;
480   my $ip_addr;
481
482   if ( $self->addr_block and $self->addr_block->routernum == $self->routernum ) {
483     # simple case: user chose a block, find an address in that block
484     # (this overrides an existing IP address if it's not in the block)
485     @blocks = ($self->addr_block);
486   }
487   elsif ( $self->routernum ) {
488     @blocks = $self->router->auto_addr_block;
489   }
490   else { 
491     return '';
492   }
493 #warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
494
495   foreach my $block ( @blocks ) {
496     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
497       # don't change anything
498       return '';
499     }
500     $ip_addr = $block->next_free_addr;
501     if ( $ip_addr ) {
502       $self->set(ip_addr => $ip_addr->addr);
503       $self->set(blocknum => $block->blocknum);
504       return '';
505     }
506   }
507   return 'No IP address available on this router';
508 }
509
510 =item assign_router
511
512 Assign an address block and router matching the selected IP address.
513 Does nothing if IP address is null.
514
515 =cut
516
517 sub assign_router {
518   my $self = shift;
519   return '' if !$self->ip_addr;
520   #warn "assigning router/block for ".$self->ip_addr."\n";
521   foreach my $router ($self->allowed_routers) {
522     foreach my $block ($router->addr_block) {
523       if ( $block->NetAddr->contains($self->NetAddr) ) {
524         $self->blocknum($block->blocknum);
525         $self->routernum($block->routernum);
526         return '';
527       }
528     }
529   }
530   return $self->ip_addr.' is not in an allowed block.';
531 }
532
533 sub _check_ip_addr {
534   my $self = shift;
535
536   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
537     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
538     return 'IP address required';
539   }
540   else {
541     return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
542   }
543
544   if ( $self->addr_block 
545       and not $self->addr_block->NetAddr->contains($self->NetAddr) ) {
546     return 'Address '.$self->ip_addr.' not in block '.$self->addr_block->cidr;
547   }
548
549 #  if (my $dup = qsearchs('svc_broadband', {
550 #        ip_addr => $self->ip_addr,
551 #        svcnum  => {op=>'!=', value => $self->svcnum}
552 #      }) ) {
553 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
554 #  }
555   '';
556 }
557
558 sub _check_duplicate {
559   my $self = shift;
560   # Not a reliable check because the table isn't locked, but 
561   # that's why we have a unique index.  This is just to give a
562   # friendlier error message.
563   my @dup;
564   @dup = $self->find_duplicates('global', 'ip_addr');
565   if ( @dup ) {
566     return "IP address in use (svcnum ".$dup[0]->svcnum.")";
567   }
568   @dup = $self->find_duplicates('global', 'mac_addr');
569   if ( @dup ) {
570     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
571   }
572
573   '';
574 }
575
576
577 =item NetAddr
578
579 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
580 is /32.
581
582 =cut
583
584 sub NetAddr {
585   my $self = shift;
586   new NetAddr::IP ($self->ip_addr);
587 }
588
589 =item addr_block
590
591 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
592
593 =cut
594
595 sub addr_block {
596   my $self = shift;
597   qsearchs('addr_block', { blocknum => $self->blocknum });
598 }
599
600 =item router
601
602 Returns the FS::router record for this service.
603
604 =cut
605
606 sub router {
607   my $self = shift;
608   qsearchs('router', { routernum => $self->routernum });
609 }
610
611 =item allowed_routers
612
613 Returns a list of allowed FS::router objects.
614
615 =cut
616
617 sub allowed_routers {
618   my $self = shift;
619   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
620   my @r = map { $_->router } qsearch('part_svc_router', 
621     { svcpart => $svcpart });
622   if ( $self->cust_main ) {
623     my $agentnum = $self->cust_main->agentnum;
624     return grep { !$_->agentnum or $_->agentnum == $agentnum } @r;
625   }
626   else {
627     return @r;
628   }
629 }
630
631 =back
632
633
634 =item mac_addr_formatted CASE DELIMITER
635
636 Format the MAC address (for use by exports).  If CASE starts with "l"
637 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
638 between octets.
639
640 =cut
641
642 sub mac_addr_formatted {
643   my $self = shift;
644   my ($case, $delim) = @_;
645   my $addr = $self->mac_addr;
646   $addr = lc($addr) if $case =~ /^l/i;
647   join( $delim || '', $addr =~ /../g );
648 }
649
650 #class method
651 sub _upgrade_data {
652   my $class = shift;
653
654   local($FS::svc_Common::noexport_hack) = 1;
655
656   # set routernum to addr_block.routernum
657   foreach my $self (qsearch('svc_broadband', {
658       blocknum => {op => '!=', value => ''},
659       routernum => ''
660     })) {
661     my $addr_block = $self->addr_block;
662     if ( !$addr_block ) {
663       # super paranoid mode
664       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
665       next;
666     }
667     my $ip_addr = $self->ip_addr;
668     my $routernum = $addr_block->routernum;
669     if ( $routernum ) {
670       $self->set(routernum => $routernum);
671       my $error = $self->check;
672       # sanity check: don't allow this to change IP address or block
673       # (other than setting blocknum to null for a non-auto-assigned router)
674       if ( $self->ip_addr ne $ip_addr 
675         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
676         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
677         next;
678       }
679
680       $error ||= $self->replace;
681       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
682           ":\n$error; skipped\n"
683         if $error;
684     }
685     else {
686       warn "svcnum ".$self->svcnum.
687         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
688     }
689   }
690
691   # assign blocknums to services that should have them
692   my @all_blocks = qsearch('addr_block', { });
693   SVC: foreach my $self ( 
694     qsearch({
695         'select' => 'svc_broadband.*',
696         'table' => 'svc_broadband',
697         'addl_from' => 'JOIN router USING (routernum)',
698         'hashref' => {},
699         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
700                        'AND router.manual_addr IS NULL',
701     }) 
702   ) {
703    
704     next SVC if $self->ip_addr eq '';
705     my $NetAddr = $self->NetAddr;
706     # inefficient, but should only need to run once
707     foreach my $block (@all_blocks) {
708       if ($block->NetAddr->contains($NetAddr)) {
709         $self->set(blocknum => $block->blocknum);
710         my $error = $self->replace;
711         warn "WARNING: error assigning blocknum ".$block->blocknum.
712         " to service ".$self->svcnum."\n$error; skipped\n"
713           if $error;
714         next SVC;
715       }
716     }
717     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
718       $self->svcnum;
719     #next SVC;
720   }
721
722   '';
723 }
724
725 =back
726
727 =head1 BUGS
728
729 The business with sb_field has been 'fixed', in a manner of speaking.
730
731 allowed_routers isn't agent virtualized because part_svc isn't agent
732 virtualized
733
734 Having both routernum and blocknum as foreign keys is somewhat dubious.
735
736 =head1 SEE ALSO
737
738 FS::svc_Common, FS::Record, FS::addr_block,
739 FS::part_svc, schema.html from the base documentation.
740
741 =cut
742
743 1;
744