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