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