sqlradius data volume report improvements, #18823
[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   #exportnum
249   if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
250     push @from, 'LEFT JOIN export_svc USING ( svcpart )';
251     push @where, "exportnum = $1";
252   }
253
254   #ip_addr
255   if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
256     push @where, "ip_addr = '$1'";
257   }
258
259   #custnum
260   if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
261     push @where, "custnum = $1";
262   }
263   
264   my $addl_from = join(' ', @from);
265   my $extra_sql = '';
266   $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
267   my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
268   return( {
269       'table'   => 'svc_broadband',
270       'hashref' => {},
271       'select'  => join(', ',
272         'svc_broadband.*',
273         'part_svc.svc',
274         'cust_main.custnum',
275         FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
276       ),
277       'extra_sql' => $extra_sql,
278       'addl_from' => $addl_from,
279       'order_by'  => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
280       'count_query' => $count_query,
281     } );
282 }
283
284 =item search_sql STRING
285
286 Class method which returns an SQL fragment to search for the given string.
287
288 =cut
289
290 sub search_sql {
291   my( $class, $string ) = @_;
292   if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
293     $class->search_sql_field('ip_addr', $string );
294   }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
295     $class->search_sql_field('mac_addr', uc($string));
296   }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
297     $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
298   } else {
299     '1 = 0'; #false
300   }
301 }
302
303 =item label
304
305 Returns the IP address.
306
307 =cut
308
309 sub label {
310   my $self = shift;
311   $self->ip_addr;
312 }
313
314 =item insert [ , OPTION => VALUE ... ]
315
316 Adds this record to the database.  If there is an error, returns the error,
317 otherwise returns false.
318
319 The additional fields pkgnum and svcpart (see FS::cust_svc) should be 
320 defined.  An FS::cust_svc record will be created and inserted.
321
322 Currently available options are: I<depend_jobnum>
323
324 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
325 jobnums), all provisioning jobs will have a dependancy on the supplied
326 jobnum(s) (they will not run until the specific job(s) complete(s)).
327
328 =cut
329
330 # Standard FS::svc_Common::insert
331
332 =item delete
333
334 Delete this record from the database.
335
336 =cut
337
338 # Standard FS::svc_Common::delete
339
340 =item replace OLD_RECORD
341
342 Replaces the OLD_RECORD with this one in the database.  If there is an error,
343 returns the error, otherwise returns false.
344
345 # Standard FS::svc_Common::replace
346
347 =item suspend
348
349 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
350
351 =item unsuspend
352
353 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
354
355 =item cancel
356
357 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
358
359 =item check
360
361 Checks all fields to make sure this is a valid broadband service.  If there is
362 an error, returns the error, otherwise returns false.  Called by the insert
363 and replace methods.
364
365 =cut
366
367 sub check {
368   my $self = shift;
369   my $x = $self->setfixed;
370
371   return $x unless ref($x);
372
373   # remove delimiters
374   my $mac_addr = uc($self->get('mac_addr'));
375   $mac_addr =~ s/[-: ]//g;
376   $self->set('mac_addr', $mac_addr);
377
378   my $error =
379     $self->ut_numbern('svcnum')
380     || $self->ut_numbern('blocknum')
381     || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
382     || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
383     || $self->ut_textn('description')
384     || $self->ut_numbern('speed_up')
385     || $self->ut_numbern('speed_down')
386     || $self->ut_ipn('ip_addr')
387     || $self->ut_hexn('mac_addr')
388     || $self->ut_hexn('auth_key')
389     || $self->ut_coordn('latitude')
390     || $self->ut_coordn('longitude')
391     || $self->ut_sfloatn('altitude')
392     || $self->ut_textn('vlan_profile')
393     || $self->ut_textn('plan_id')
394   ;
395   return $error if $error;
396
397   if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
398   if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
399
400   my $cust_svc = $self->svcnum
401                  ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
402                  : '';
403   my $cust_pkg;
404   my $svcpart;
405   if ($cust_svc) {
406     $cust_pkg = $cust_svc->cust_pkg;
407     $svcpart = $cust_svc->svcpart;
408   }else{
409     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
410     return "Invalid pkgnum" unless $cust_pkg;
411     $svcpart = $self->svcpart;
412   }
413   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
414
415   if ( $conf->exists('auto_router') and $self->ip_addr and !$self->routernum ) {
416     # assign_router is guaranteed to provide a router that's legal
417     # for this agent and svcpart
418     my $error = $self->_check_ip_addr || $self->assign_router;
419     return $error if $error;
420   }
421   elsif ($self->routernum) {
422     return "Router ".$self->routernum." does not provide this service"
423       unless qsearchs('part_svc_router', { 
424         svcpart => $svcpart,
425         routernum => $self->routernum
426     });
427   
428     my $router = $self->router;
429     return "Router ".$self->routernum." does not serve this customer"
430       if $router->agentnum and $agentnum and $router->agentnum != $agentnum;
431
432     if ( $router->manual_addr ) {
433       $self->blocknum('');
434     }
435     else {
436       my $addr_block = $self->addr_block;
437       if ( $self->ip_addr eq '' 
438            and not ( $addr_block and $addr_block->manual_flag ) ) {
439         my $error = $self->assign_ip_addr;
440         return $error if $error;
441       }
442     }
443  
444     my $error = $self->_check_ip_addr;
445     return $error if $error;
446   } # if $self->routernum
447
448   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
449     my $l = $cust_pkg->cust_location_or_main;
450     if ( $l->ship_latitude && $l->ship_longitude ) {
451       $self->latitude(  $l->ship_latitude  );
452       $self->longitude( $l->ship_longitude );
453     } elsif ( $l->latitude && $l->longitude ) {
454       $self->latitude(  $l->latitude  );
455       $self->longitude( $l->longitude );
456     }
457   }
458
459   $self->SUPER::check;
460 }
461
462 =item assign_ip_addr
463
464 Assign an IP address matching the selected router, and the selected block
465 if there is one.
466
467 =cut
468
469 sub assign_ip_addr {
470   my $self = shift;
471   my @blocks;
472   my $ip_addr;
473
474   if ( $self->addr_block and $self->addr_block->routernum == $self->routernum ) {
475     # simple case: user chose a block, find an address in that block
476     # (this overrides an existing IP address if it's not in the block)
477     @blocks = ($self->addr_block);
478   }
479   elsif ( $self->routernum ) {
480     @blocks = $self->router->auto_addr_block;
481   }
482   else { 
483     return '';
484   }
485 #warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
486
487   foreach my $block ( @blocks ) {
488     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
489       # don't change anything
490       return '';
491     }
492     $ip_addr = $block->next_free_addr;
493     if ( $ip_addr ) {
494       $self->set(ip_addr => $ip_addr->addr);
495       $self->set(blocknum => $block->blocknum);
496       return '';
497     }
498   }
499   return 'No IP address available on this router';
500 }
501
502 =item assign_router
503
504 Assign an address block and router matching the selected IP address.
505 Does nothing if IP address is null.
506
507 =cut
508
509 sub assign_router {
510   my $self = shift;
511   return '' if !$self->ip_addr;
512   #warn "assigning router/block for ".$self->ip_addr."\n";
513   foreach my $router ($self->allowed_routers) {
514     foreach my $block ($router->addr_block) {
515       if ( $block->NetAddr->contains($self->NetAddr) ) {
516         $self->blocknum($block->blocknum);
517         $self->routernum($block->routernum);
518         return '';
519       }
520     }
521   }
522   return $self->ip_addr.' is not in an allowed block.';
523 }
524
525 sub _check_ip_addr {
526   my $self = shift;
527
528   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
529     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
530     return 'IP address required';
531   }
532   else {
533     return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
534   }
535
536   if ( $self->addr_block 
537       and not $self->addr_block->NetAddr->contains($self->NetAddr) ) {
538     return 'Address '.$self->ip_addr.' not in block '.$self->addr_block->cidr;
539   }
540
541 #  if (my $dup = qsearchs('svc_broadband', {
542 #        ip_addr => $self->ip_addr,
543 #        svcnum  => {op=>'!=', value => $self->svcnum}
544 #      }) ) {
545 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
546 #  }
547   '';
548 }
549
550 sub _check_duplicate {
551   my $self = shift;
552   # Not a reliable check because the table isn't locked, but 
553   # that's why we have a unique index.  This is just to give a
554   # friendlier error message.
555   my @dup;
556   @dup = $self->find_duplicates('global', 'ip_addr');
557   if ( @dup ) {
558     return "IP address in use (svcnum ".$dup[0]->svcnum.")";
559   }
560   @dup = $self->find_duplicates('global', 'mac_addr');
561   if ( @dup ) {
562     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
563   }
564
565   '';
566 }
567
568
569 =item NetAddr
570
571 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
572 is /32.
573
574 =cut
575
576 sub NetAddr {
577   my $self = shift;
578   new NetAddr::IP ($self->ip_addr);
579 }
580
581 =item addr_block
582
583 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
584
585 =cut
586
587 sub addr_block {
588   my $self = shift;
589   qsearchs('addr_block', { blocknum => $self->blocknum });
590 }
591
592 =item router
593
594 Returns the FS::router record for this service.
595
596 =cut
597
598 sub router {
599   my $self = shift;
600   qsearchs('router', { routernum => $self->routernum });
601 }
602
603 =item allowed_routers
604
605 Returns a list of allowed FS::router objects.
606
607 =cut
608
609 sub allowed_routers {
610   my $self = shift;
611   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
612   my @r = map { $_->router } qsearch('part_svc_router', 
613     { svcpart => $svcpart });
614   if ( $self->cust_main ) {
615     my $agentnum = $self->cust_main->agentnum;
616     return grep { !$_->agentnum or $_->agentnum == $agentnum } @r;
617   }
618   else {
619     return @r;
620   }
621 }
622
623 =back
624
625
626 =item mac_addr_formatted CASE DELIMITER
627
628 Format the MAC address (for use by exports).  If CASE starts with "l"
629 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
630 between octets.
631
632 =cut
633
634 sub mac_addr_formatted {
635   my $self = shift;
636   my ($case, $delim) = @_;
637   my $addr = $self->mac_addr;
638   $addr = lc($addr) if $case =~ /^l/i;
639   join( $delim || '', $addr =~ /../g );
640 }
641
642 #class method
643 sub _upgrade_data {
644   my $class = shift;
645
646   local($FS::svc_Common::noexport_hack) = 1;
647
648   # set routernum to addr_block.routernum
649   foreach my $self (qsearch('svc_broadband', {
650       blocknum => {op => '!=', value => ''},
651       routernum => ''
652     })) {
653     my $addr_block = $self->addr_block;
654     if ( !$addr_block ) {
655       # super paranoid mode
656       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
657       next;
658     }
659     my $ip_addr = $self->ip_addr;
660     my $routernum = $addr_block->routernum;
661     if ( $routernum ) {
662       $self->set(routernum => $routernum);
663       my $error = $self->check;
664       # sanity check: don't allow this to change IP address or block
665       # (other than setting blocknum to null for a non-auto-assigned router)
666       if ( $self->ip_addr ne $ip_addr 
667         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
668         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
669         next;
670       }
671
672       $error ||= $self->replace;
673       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
674           ":\n$error; skipped\n"
675         if $error;
676     }
677     else {
678       warn "svcnum ".$self->svcnum.
679         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
680     }
681   }
682
683   # assign blocknums to services that should have them
684   my @all_blocks = qsearch('addr_block', { });
685   SVC: foreach my $self ( 
686     qsearch({
687         'select' => 'svc_broadband.*',
688         'table' => 'svc_broadband',
689         'addl_from' => 'JOIN router USING (routernum)',
690         'hashref' => {},
691         'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
692                        'AND router.manual_addr IS NULL',
693     }) 
694   ) {
695    
696     next SVC if $self->ip_addr eq '';
697     my $NetAddr = $self->NetAddr;
698     # inefficient, but should only need to run once
699     foreach my $block (@all_blocks) {
700       if ($block->NetAddr->contains($NetAddr)) {
701         $self->set(blocknum => $block->blocknum);
702         my $error = $self->replace;
703         warn "WARNING: error assigning blocknum ".$block->blocknum.
704         " to service ".$self->svcnum."\n$error; skipped\n"
705           if $error;
706         next SVC;
707       }
708     }
709     warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
710       $self->svcnum;
711     #next SVC;
712   }
713
714   '';
715 }
716
717 =back
718
719 =head1 BUGS
720
721 The business with sb_field has been 'fixed', in a manner of speaking.
722
723 allowed_routers isn't agent virtualized because part_svc isn't agent
724 virtualized
725
726 Having both routernum and blocknum as foreign keys is somewhat dubious.
727
728 =head1 SEE ALSO
729
730 FS::svc_Common, FS::Record, FS::addr_block,
731 FS::part_svc, schema.html from the base documentation.
732
733 =cut
734
735 1;
736