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