allow svc_broadband to link directly to a router, #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 { ( '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   if ($cust_svc) {
399     $cust_pkg = $cust_svc->cust_pkg;
400   }else{
401     $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
402     return "Invalid pkgnum" unless $cust_pkg;
403   }
404   my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
405
406   if ($self->routernum) {
407     return "Router ".$self->routernum." does not provide this service"
408       unless qsearchs('part_svc_router', { 
409         svcpart => $self->cust_svc->svcpart,
410         routernum => $self->routernum
411     });
412   
413     my $router = $self->router;
414     return "Router ".$self->routernum." does not serve this customer"
415       if $router->agentnum and $router->agentnum != $agentnum;
416
417     if ( $router->auto_addr ) {
418       my $error = $self->assign_ip_addr;
419       return $error if $error;
420     }
421     else {
422       $self->blocknum('');
423     }
424   } # if $self->routernum
425
426   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
427     my $l = $cust_pkg->cust_location_or_main;
428     if ( $l->ship_latitude && $l->ship_longitude ) {
429       $self->latitude(  $l->ship_latitude  );
430       $self->longitude( $l->ship_longitude );
431     } elsif ( $l->latitude && $l->longitude ) {
432       $self->latitude(  $l->latitude  );
433       $self->longitude( $l->longitude );
434     }
435   }
436
437   $error = $self->_check_ip_addr;
438   return $error if $error;
439
440   $self->SUPER::check;
441 }
442
443 =item assign_ip_addr
444
445 Assign an address block matching the selected router, and the selected block
446 if there is one.
447
448 =cut
449
450 sub assign_ip_addr {
451   my $self = shift;
452   my @blocks;
453   my $ip_addr;
454
455   if ( $self->blocknum and $self->addr_block->routernum == $self->routernum ) {
456     # simple case: user chose a block, find an address in that block
457     # (this overrides an existing IP address if it's not in the block)
458     @blocks = ($self->addr_block);
459   }
460   elsif ( $self->routernum ) {
461     @blocks = $self->router->auto_addr_block;
462   }
463   else { 
464     return '';
465   }
466
467   foreach my $block ( @blocks ) {
468     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
469       # don't change anything
470       return '';
471     }
472     $ip_addr = $block->next_free_addr;
473     last if $ip_addr;
474   }
475   if ( $ip_addr ) {
476     $self->set(ip_addr => $ip_addr->addr);
477     return '';
478   }
479   else {
480     return 'No IP address available on this router';
481   }
482 }
483
484 sub _check_ip_addr {
485   my $self = shift;
486
487   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
488     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
489     return 'IP address required';
490   }
491 #  if (my $dup = qsearchs('svc_broadband', {
492 #        ip_addr => $self->ip_addr,
493 #        svcnum  => {op=>'!=', value => $self->svcnum}
494 #      }) ) {
495 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
496 #  }
497   '';
498 }
499
500 sub _check_duplicate {
501   my $self = shift;
502
503   return "MAC already in use"
504     if ( $self->mac_addr &&
505          scalar( qsearch( 'svc_broadband', { 'mac_addr', $self->mac_addr } ) )
506        );
507
508   '';
509 }
510
511
512 =item NetAddr
513
514 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
515 is /32.
516
517 =cut
518
519 sub NetAddr {
520   my $self = shift;
521   new NetAddr::IP ($self->ip_addr);
522 }
523
524 =item addr_block
525
526 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
527
528 =cut
529
530 sub addr_block {
531   my $self = shift;
532   qsearchs('addr_block', { blocknum => $self->blocknum });
533 }
534
535 =item router
536
537 Returns the FS::router record for this service.
538
539 =cut
540
541 sub router {
542   my $self = shift;
543   qsearchs('router', { routernum => $self->routernum });
544 }
545
546 =back
547
548 =item allowed_routers
549
550 Returns a list of allowed FS::router objects.
551
552 =cut
553
554 sub allowed_routers {
555   my $self = shift;
556   map { $_->router } qsearch('part_svc_router', 
557     { svcpart => $self->cust_svc->svcpart });
558 }
559
560
561 #class method
562 sub _upgrade_data {
563   my $class = shift;
564
565   # set routernum to addr_block.routernum
566   foreach my $self (qsearch('svc_broadband', {
567       blocknum => {op => '!=', value => ''},
568       routernum => ''
569     })) {
570     my $addr_block = $self->addr_block;
571     if ( my $routernum = $addr_block->routernum ) {
572       $self->set(routernum => $routernum);
573       my $error = $self->replace;
574       die "error assigning routernum $routernum to service ".$self->svcnum.
575           ":\n$error\n"
576         if $error;
577     }
578     else {
579       warn "svcnum ".$self->svcnum.
580         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
581     }
582   }
583   '';
584 }
585
586 =head1 BUGS
587
588 The business with sb_field has been 'fixed', in a manner of speaking.
589
590 allowed_routers isn't agent virtualized because part_svc isn't agent
591 virtualized
592
593 Having both routernum and blocknum as foreign keys is somewhat dubious.
594
595 =head1 SEE ALSO
596
597 FS::svc_Common, FS::Record, FS::addr_block,
598 FS::part_svc, schema.html from the base documentation.
599
600 =cut
601
602 1;
603