warn during upgrade if addr_block records are missing, #17040
[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 { ( 'ip_addr', '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 ( $conf->exists('auto_router') and $self->ip_addr and !$self->routernum ) {
409     # assign_router is guaranteed to provide a router that's legal
410     # for this agent and svcpart
411     my $error = $self->_check_ip_addr || $self->assign_router;
412     return $error if $error;
413   }
414   elsif ($self->routernum) {
415     return "Router ".$self->routernum." does not provide this service"
416       unless qsearchs('part_svc_router', { 
417         svcpart => $svcpart,
418         routernum => $self->routernum
419     });
420   
421     my $router = $self->router;
422     return "Router ".$self->routernum." does not serve this customer"
423       if $router->agentnum and $agentnum and $router->agentnum != $agentnum;
424
425     if ( $router->manual_addr ) {
426       $self->blocknum('');
427     }
428     else {
429       my $addr_block = $self->addr_block;
430       unless ( $addr_block and $addr_block->manual_flag ) {
431         my $error = $self->assign_ip_addr;
432         return $error if $error;
433       }
434     }
435  
436     my $error = $self->_check_ip_addr;
437     return $error if $error;
438   } # if $self->routernum
439
440   if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
441     my $l = $cust_pkg->cust_location_or_main;
442     if ( $l->ship_latitude && $l->ship_longitude ) {
443       $self->latitude(  $l->ship_latitude  );
444       $self->longitude( $l->ship_longitude );
445     } elsif ( $l->latitude && $l->longitude ) {
446       $self->latitude(  $l->latitude  );
447       $self->longitude( $l->longitude );
448     }
449   }
450
451   $self->SUPER::check;
452 }
453
454 =item assign_ip_addr
455
456 Assign an IP address matching the selected router, and the selected block
457 if there is one.
458
459 =cut
460
461 sub assign_ip_addr {
462   my $self = shift;
463   my @blocks;
464   my $ip_addr;
465
466   if ( $self->blocknum and $self->addr_block->routernum == $self->routernum ) {
467     # simple case: user chose a block, find an address in that block
468     # (this overrides an existing IP address if it's not in the block)
469     @blocks = ($self->addr_block);
470   }
471   elsif ( $self->routernum ) {
472     @blocks = $self->router->auto_addr_block;
473   }
474   else { 
475     return '';
476   }
477 #warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
478
479   foreach my $block ( @blocks ) {
480     if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
481       # don't change anything
482       return '';
483     }
484     $ip_addr = $block->next_free_addr;
485     last if $ip_addr;
486   }
487   if ( $ip_addr ) {
488     $self->set(ip_addr => $ip_addr->addr);
489     return '';
490   }
491   else {
492     return 'No IP address available on this router';
493   }
494 }
495
496 =item assign_router
497
498 Assign an address block and router matching the selected IP address.
499 Does nothing if IP address is null.
500
501 =cut
502
503 sub assign_router {
504   my $self = shift;
505   return '' if !$self->ip_addr;
506   #warn "assigning router/block for ".$self->ip_addr."\n";
507   foreach my $router ($self->allowed_routers) {
508     foreach my $block ($router->addr_block) {
509       if ( $block->NetAddr->contains($self->NetAddr) ) {
510         $self->blocknum($block->blocknum);
511         $self->routernum($block->routernum);
512         return '';
513       }
514     }
515   }
516   return $self->ip_addr.' is not in an allowed block.';
517 }
518
519 sub _check_ip_addr {
520   my $self = shift;
521
522   if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
523     return '' if $conf->exists('svc_broadband-allow_null_ip_addr'); 
524     return 'IP address required';
525   }
526   else {
527     return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
528   }
529 #  if (my $dup = qsearchs('svc_broadband', {
530 #        ip_addr => $self->ip_addr,
531 #        svcnum  => {op=>'!=', value => $self->svcnum}
532 #      }) ) {
533 #    return 'IP address conflicts with svcnum '.$dup->svcnum;
534 #  }
535   '';
536 }
537
538 sub _check_duplicate {
539   my $self = shift;
540
541   $self->lock_table;
542
543   my @dup;
544   @dup = $self->find_duplicates('global', 'ip_addr');
545   if ( @dup ) {
546     return "IP address in use (svcnum ".$dup[0]->svcnum.")";
547   }
548   @dup = $self->find_duplicates('global', 'mac_addr');
549   if ( @dup ) {
550     return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
551   }
552
553   '';
554 }
555
556
557 =item NetAddr
558
559 Returns a NetAddr::IP object containing the IP address of this service.  The netmask 
560 is /32.
561
562 =cut
563
564 sub NetAddr {
565   my $self = shift;
566   new NetAddr::IP ($self->ip_addr);
567 }
568
569 =item addr_block
570
571 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
572
573 =cut
574
575 sub addr_block {
576   my $self = shift;
577   qsearchs('addr_block', { blocknum => $self->blocknum });
578 }
579
580 =item router
581
582 Returns the FS::router record for this service.
583
584 =cut
585
586 sub router {
587   my $self = shift;
588   qsearchs('router', { routernum => $self->routernum });
589 }
590
591 =item allowed_routers
592
593 Returns a list of allowed FS::router objects.
594
595 =cut
596
597 sub allowed_routers {
598   my $self = shift;
599   my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
600   my @r = map { $_->router } qsearch('part_svc_router', 
601     { svcpart => $svcpart });
602   if ( $self->cust_main ) {
603     my $agentnum = $self->cust_main->agentnum;
604     return grep { !$_->agentnum or $_->agentnum == $agentnum } @r;
605   }
606   else {
607     return @r;
608   }
609 }
610
611 =back
612
613
614 =item mac_addr_formatted CASE DELIMITER
615
616 Format the MAC address (for use by exports).  If CASE starts with "l"
617 (for "lowercase"), it's returned in lowercase.  DELIMITER is inserted
618 between octets.
619
620 =cut
621
622 sub mac_addr_formatted {
623   my $self = shift;
624   my ($case, $delim) = @_;
625   my $addr = $self->mac_addr;
626   $addr = lc($addr) if $case =~ /^l/i;
627   join( $delim || '', $addr =~ /../g );
628 }
629
630 #class method
631 sub _upgrade_data {
632   my $class = shift;
633
634   local($FS::svc_Common::noexport_hack) = 1;
635
636   # set routernum to addr_block.routernum
637   foreach my $self (qsearch('svc_broadband', {
638       blocknum => {op => '!=', value => ''},
639       routernum => ''
640     })) {
641     my $addr_block = $self->addr_block;
642     if ( !$addr_block ) {
643       # super paranoid mode
644       warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
645       next;
646     }
647     my $ip_addr = $self->ip_addr;
648     my $routernum = $addr_block->routernum;
649     if ( $routernum ) {
650       $self->set(routernum => $routernum);
651       my $error = $self->check;
652       # sanity check: don't allow this to change IP address or block
653       # (other than setting blocknum to null for a non-auto-assigned router)
654       if ( $self->ip_addr ne $ip_addr 
655         or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
656         warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
657         next;
658       }
659
660       $error ||= $self->replace;
661       warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
662           ":\n$error; skipped\n"
663         if $error;
664     }
665     else {
666       warn "svcnum ".$self->svcnum.
667         ": no routernum in address block ".$addr_block->cidr.", skipped\n";
668     }
669   }
670   '';
671 }
672
673 =back
674
675 =head1 BUGS
676
677 The business with sb_field has been 'fixed', in a manner of speaking.
678
679 allowed_routers isn't agent virtualized because part_svc isn't agent
680 virtualized
681
682 Having both routernum and blocknum as foreign keys is somewhat dubious.
683
684 =head1 SEE ALSO
685
686 FS::svc_Common, FS::Record, FS::addr_block,
687 FS::part_svc, schema.html from the base documentation.
688
689 =cut
690
691 1;
692