1 package FS::svc_broadband;
4 use vars qw(@ISA $conf);
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 );
12 use FS::part_svc_router;
15 $FS::UID::callback{'FS::svc_broadband'} = sub {
21 FS::svc_broadband - Object methods for svc_broadband records
25 use FS::svc_broadband;
27 $record = new FS::svc_broadband \%hash;
28 $record = new FS::svc_broadband { 'column' => 'value' };
30 $error = $record->insert;
32 $error = $new_record->replace($old_record);
34 $error = $record->delete;
36 $error = $record->check;
38 $error = $record->suspend;
40 $error = $record->unsuspend;
42 $error = $record->cancel;
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:
50 FS::svc_broadband inherits from FS::svc_Common. The following fields are
55 =item svcnum - primary key
57 =item blocknum - see FS::addr_block
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
66 speed_down - maximum download speed, as above
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
84 Creates a new svc_broadband. To add the record to the database, see
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.
94 'name' => 'Wireless broadband',
95 'name_plural' => 'Wireless broadband services',
96 'longname_plural' => 'Fixed wireless broadband services',
97 'display_weight' => 50,
98 'cancel_weight' => 70,
99 'ip_field' => 'ip_addr',
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.',
107 { 'label' => 'Address block',
109 'select_table' => 'addr_block',
110 'select_key' => 'blocknum',
111 'select_label' => 'cidr',
112 'disable_inventory' => 1,
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',
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,
137 sub table { 'svc_broadband'; }
139 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
143 Class method which returns a qsearch hash expression to search for parameters
144 specified in HASHREF.
150 =item unlinked - set to search for all unlinked services. Overrides all other options.
160 =item pkgpart - arrayref
162 =item routernum - arrayref
164 =item sectornum - arrayref
166 =item towernum - arrayref
175 my ($class, $params) = @_;
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 )',
184 # based on FS::svc_acct::search, probably the most mature of the bunch
186 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
189 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
190 push @where, "cust_main.agentnum = $1";
192 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
193 'null_right' => 'View/link unlinked services',
194 'table' => 'cust_main'
198 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
199 push @where, "custnum = $1";
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;
208 elsif ( $pkgpart =~ /^(\d+)$/ ) {
209 push @where, "cust_pkg.pkgpart = $1";
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 );
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).' )';
224 elsif ( $routernum =~ /^(\d+)$/ ) {
225 push @where, "svc_broadband.routernum = $1";
227 elsif ( $routernum eq 'none' ) {
228 push @where, "svc_broadband.routernum IS NULL";
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 )';
240 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
241 push @where, "svcnum = $1";
245 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
246 push @where, "svcpart = $1";
250 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
251 push @from, 'LEFT JOIN export_svc USING ( svcpart )';
252 push @where, "exportnum = $1";
256 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
257 push @where, "ip_addr = '$1'";
261 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
262 push @where, "custnum = $1";
265 my $addl_from = join(' ', @from);
267 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
268 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
270 'table' => 'svc_broadband',
272 'select' => join(', ',
276 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
278 'extra_sql' => $extra_sql,
279 'addl_from' => $addl_from,
280 'order_by' => ($params->{'order_by'} || 'ORDER BY svcnum'),
281 'count_query' => $count_query,
285 =item search_sql STRING
287 Class method which returns an SQL fragment to search for the given string.
292 my( $class, $string ) = @_;
293 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
294 $class->search_sql_field('ip_addr', $string );
295 } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
296 $class->search_sql_field('mac_addr', uc($string));
297 } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
299 $class->search_sql_field('mac_addr', uc($string) );
300 } elsif ( $string =~ /^(\d+)$/ ) {
301 my $table = $class->table;
302 "$table.svcnum = $1";
310 Returns the IP address.
316 my $label = 'IP:'. ($self->ip_addr || 'Unknown');
317 $label .= ', MAC:'. $self->mac_addr
319 $label .= ' ('. $self->description. ')'
320 if $self->description;
324 =item insert [ , OPTION => VALUE ... ]
326 Adds this record to the database. If there is an error, returns the error,
327 otherwise returns false.
329 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
330 defined. An FS::cust_svc record will be created and inserted.
332 Currently available options are: I<depend_jobnum>
334 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
335 jobnums), all provisioning jobs will have a dependancy on the supplied
336 jobnum(s) (they will not run until the specific job(s) complete(s)).
338 # Standard FS::svc_Common::insert
342 Delete this record from the database.
346 # Standard FS::svc_Common::delete
348 =item replace OLD_RECORD
350 Replaces the OLD_RECORD with this one in the database. If there is an error,
351 returns the error, otherwise returns false.
353 # Standard FS::svc_Common::replace
357 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
361 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
365 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
369 Checks all fields to make sure this is a valid broadband service. If there is
370 an error, returns the error, otherwise returns false. Called by the insert
377 my $x = $self->setfixed;
379 return $x unless ref($x);
382 my $mac_addr = uc($self->get('mac_addr'));
383 $mac_addr =~ s/[\W_]//g;
384 $self->set('mac_addr', $mac_addr);
387 $self->ut_numbern('svcnum')
388 || $self->ut_numbern('blocknum')
389 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
390 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
391 || $self->ut_textn('description')
392 || $self->ut_numbern('speed_up')
393 || $self->ut_numbern('speed_down')
394 || $self->ut_ipn('ip_addr')
395 || $self->ut_hexn('mac_addr')
396 || $self->ut_hexn('auth_key')
397 || $self->ut_coordn('latitude')
398 || $self->ut_coordn('longitude')
399 || $self->ut_sfloatn('altitude')
400 || $self->ut_textn('vlan_profile')
401 || $self->ut_textn('plan_id')
403 return $error if $error;
405 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
406 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
408 my $cust_svc = $self->svcnum
409 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
414 $cust_pkg = $cust_svc->cust_pkg;
415 $svcpart = $cust_svc->svcpart;
417 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
418 return "Invalid pkgnum" unless $cust_pkg;
419 $svcpart = $self->svcpart;
421 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
423 if ( $conf->exists('auto_router') and $self->ip_addr and !$self->routernum ) {
424 # assign_router is guaranteed to provide a router that's legal
425 # for this agent and svcpart
426 my $error = $self->_check_ip_addr || $self->assign_router;
427 return $error if $error;
429 elsif ($self->routernum) {
430 return "Router ".$self->routernum." does not provide this service"
431 unless qsearchs('part_svc_router', {
433 routernum => $self->routernum
436 my $router = $self->router;
437 return "Router ".$self->routernum." does not serve this customer"
438 if $router->agentnum and $agentnum and $router->agentnum != $agentnum;
440 if ( $router->manual_addr ) {
444 my $addr_block = $self->addr_block;
445 if ( $self->ip_addr eq ''
446 and not ( $addr_block and $addr_block->manual_flag ) ) {
447 my $error = $self->assign_ip_addr;
448 return $error if $error;
452 my $error = $self->_check_ip_addr;
453 return $error if $error;
454 } # if $self->routernum
456 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
457 my $l = $cust_pkg->cust_location_or_main;
458 if ( $l->ship_latitude && $l->ship_longitude ) {
459 $self->latitude( $l->ship_latitude );
460 $self->longitude( $l->ship_longitude );
461 } elsif ( $l->latitude && $l->longitude ) {
462 $self->latitude( $l->latitude );
463 $self->longitude( $l->longitude );
472 Assign an IP address matching the selected router, and the selected block
482 if ( $self->addr_block and $self->addr_block->routernum == $self->routernum ) {
483 # simple case: user chose a block, find an address in that block
484 # (this overrides an existing IP address if it's not in the block)
485 @blocks = ($self->addr_block);
487 elsif ( $self->routernum ) {
488 @blocks = $self->router->auto_addr_block;
493 #warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
495 foreach my $block ( @blocks ) {
496 if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
497 # don't change anything
500 $ip_addr = $block->next_free_addr;
502 $self->set(ip_addr => $ip_addr->addr);
503 $self->set(blocknum => $block->blocknum);
507 return 'No IP address available on this router';
512 Assign an address block and router matching the selected IP address.
513 Does nothing if IP address is null.
519 return '' if !$self->ip_addr;
520 #warn "assigning router/block for ".$self->ip_addr."\n";
521 foreach my $router ($self->allowed_routers) {
522 foreach my $block ($router->addr_block) {
523 if ( $block->NetAddr->contains($self->NetAddr) ) {
524 $self->blocknum($block->blocknum);
525 $self->routernum($block->routernum);
530 return $self->ip_addr.' is not in an allowed block.';
536 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
537 return '' if $conf->exists('svc_broadband-allow_null_ip_addr');
538 return 'IP address required';
541 return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
544 if ( $self->addr_block
545 and not $self->addr_block->NetAddr->contains($self->NetAddr) ) {
546 return 'Address '.$self->ip_addr.' not in block '.$self->addr_block->cidr;
549 # if (my $dup = qsearchs('svc_broadband', {
550 # ip_addr => $self->ip_addr,
551 # svcnum => {op=>'!=', value => $self->svcnum}
553 # return 'IP address conflicts with svcnum '.$dup->svcnum;
558 sub _check_duplicate {
560 # Not a reliable check because the table isn't locked, but
561 # that's why we have a unique index. This is just to give a
562 # friendlier error message.
564 @dup = $self->find_duplicates('global', 'ip_addr');
566 return "IP address in use (svcnum ".$dup[0]->svcnum.")";
568 @dup = $self->find_duplicates('global', 'mac_addr');
570 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
579 Returns a NetAddr::IP object containing the IP address of this service. The netmask
586 new NetAddr::IP ($self->ip_addr);
591 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
597 qsearchs('addr_block', { blocknum => $self->blocknum });
602 Returns the FS::router record for this service.
608 qsearchs('router', { routernum => $self->routernum });
611 =item allowed_routers
613 Returns a list of allowed FS::router objects.
617 sub allowed_routers {
619 my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
620 my @r = map { $_->router } qsearch('part_svc_router',
621 { svcpart => $svcpart });
622 if ( $self->cust_main ) {
623 my $agentnum = $self->cust_main->agentnum;
624 return grep { !$_->agentnum or $_->agentnum == $agentnum } @r;
634 =item mac_addr_formatted CASE DELIMITER
636 Format the MAC address (for use by exports). If CASE starts with "l"
637 (for "lowercase"), it's returned in lowercase. DELIMITER is inserted
642 sub mac_addr_formatted {
644 my ($case, $delim) = @_;
645 my $addr = $self->mac_addr;
646 $addr = lc($addr) if $case =~ /^l/i;
647 join( $delim || '', $addr =~ /../g );
654 local($FS::svc_Common::noexport_hack) = 1;
656 # set routernum to addr_block.routernum
657 foreach my $self (qsearch('svc_broadband', {
658 blocknum => {op => '!=', value => ''},
661 my $addr_block = $self->addr_block;
662 if ( !$addr_block ) {
663 # super paranoid mode
664 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
667 my $ip_addr = $self->ip_addr;
668 my $routernum = $addr_block->routernum;
670 $self->set(routernum => $routernum);
671 my $error = $self->check;
672 # sanity check: don't allow this to change IP address or block
673 # (other than setting blocknum to null for a non-auto-assigned router)
674 if ( $self->ip_addr ne $ip_addr
675 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
676 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
680 $error ||= $self->replace;
681 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
682 ":\n$error; skipped\n"
686 warn "svcnum ".$self->svcnum.
687 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
691 # assign blocknums to services that should have them
692 my @all_blocks = qsearch('addr_block', { });
693 SVC: foreach my $self (
695 'select' => 'svc_broadband.*',
696 'table' => 'svc_broadband',
697 'addl_from' => 'JOIN router USING (routernum)',
699 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
700 'AND router.manual_addr IS NULL',
704 next SVC if $self->ip_addr eq '';
705 my $NetAddr = $self->NetAddr;
706 # inefficient, but should only need to run once
707 foreach my $block (@all_blocks) {
708 if ($block->NetAddr->contains($NetAddr)) {
709 $self->set(blocknum => $block->blocknum);
710 my $error = $self->replace;
711 warn "WARNING: error assigning blocknum ".$block->blocknum.
712 " to service ".$self->svcnum."\n$error; skipped\n"
717 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
729 The business with sb_field has been 'fixed', in a manner of speaking.
731 allowed_routers isn't agent virtualized because part_svc isn't agent
734 Having both routernum and blocknum as foreign keys is somewhat dubious.
738 FS::svc_Common, FS::Record, FS::addr_block,
739 FS::part_svc, schema.html from the base documentation.