1 package FS::svc_broadband;
2 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 );
11 use FS::part_svc_router;
14 $FS::UID::callback{'FS::svc_broadband'} = sub {
20 FS::svc_broadband - Object methods for svc_broadband records
24 use FS::svc_broadband;
26 $record = new FS::svc_broadband \%hash;
27 $record = new FS::svc_broadband { 'column' => 'value' };
29 $error = $record->insert;
31 $error = $new_record->replace($old_record);
33 $error = $record->delete;
35 $error = $record->check;
37 $error = $record->suspend;
39 $error = $record->unsuspend;
41 $error = $record->cancel;
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:
49 FS::svc_broadband inherits from FS::svc_Common. The following fields are
54 =item svcnum - primary key
56 =item blocknum - see FS::addr_block
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
65 speed_down - maximum download speed, as above
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
83 Creates a new svc_broadband. To add the record to the database, see
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.
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',
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.',
106 { 'label' => 'Address block',
108 'select_table' => 'addr_block',
109 'select_key' => 'blocknum',
110 'select_label' => 'cidr',
111 'disable_inventory' => 1,
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',
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,
136 sub table { 'svc_broadband'; }
138 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
142 Class method which returns a qsearch hash expression to search for parameters
143 specified in HASHREF.
149 =item unlinked - set to search for all unlinked services. Overrides all other options.
159 =item pkgpart - arrayref
161 =item routernum - arrayref
163 =item sectornum - arrayref
165 =item towernum - arrayref
174 my ($class, $params) = @_;
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 )',
183 # based on FS::svc_acct::search, probably the most mature of the bunch
185 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
188 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
189 push @where, "cust_main.agentnum = $1";
191 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
192 'null_right' => 'View/link unlinked services',
193 'table' => 'cust_main'
197 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
198 push @where, "custnum = $1";
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;
207 elsif ( $pkgpart =~ /^(\d+)$/ ) {
208 push @where, "cust_pkg.pkgpart = $1";
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 );
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).' )';
223 elsif ( $routernum =~ /^(\d+)$/ ) {
224 push @where, "svc_broadband.routernum = $1";
226 elsif ( $routernum eq 'none' ) {
227 push @where, "svc_broadband.routernum IS NULL";
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 )';
239 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
240 push @where, "svcnum = $1";
244 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
245 push @where, "svcpart = $1";
249 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
250 push @from, 'LEFT JOIN export_svc USING ( svcpart )';
251 push @where, "exportnum = $1";
255 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
256 push @where, "ip_addr = '$1'";
260 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
261 push @where, "custnum = $1";
264 my $addl_from = join(' ', @from);
266 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
267 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
269 'table' => 'svc_broadband',
271 'select' => join(', ',
275 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
277 'extra_sql' => $extra_sql,
278 'addl_from' => $addl_from,
279 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
280 'count_query' => $count_query,
284 =item search_sql STRING
286 Class method which returns an SQL fragment to search for the given string.
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") );
305 Returns the IP address.
314 =item insert [ , OPTION => VALUE ... ]
316 Adds this record to the database. If there is an error, returns the error,
317 otherwise returns false.
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.
322 Currently available options are: I<depend_jobnum>
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)).
330 # Standard FS::svc_Common::insert
334 Delete this record from the database.
338 # Standard FS::svc_Common::delete
340 =item replace OLD_RECORD
342 Replaces the OLD_RECORD with this one in the database. If there is an error,
343 returns the error, otherwise returns false.
345 # Standard FS::svc_Common::replace
349 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
353 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
357 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
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
369 my $x = $self->setfixed;
371 return $x unless ref($x);
374 my $mac_addr = uc($self->get('mac_addr'));
375 $mac_addr =~ s/[-: ]//g;
376 $self->set('mac_addr', $mac_addr);
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')
395 return $error if $error;
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'; }
400 my $cust_svc = $self->svcnum
401 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
406 $cust_pkg = $cust_svc->cust_pkg;
407 $svcpart = $cust_svc->svcpart;
409 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
410 return "Invalid pkgnum" unless $cust_pkg;
411 $svcpart = $self->svcpart;
413 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
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;
421 elsif ($self->routernum) {
422 return "Router ".$self->routernum." does not provide this service"
423 unless qsearchs('part_svc_router', {
425 routernum => $self->routernum
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;
432 if ( $router->manual_addr ) {
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;
444 my $error = $self->_check_ip_addr;
445 return $error if $error;
446 } # if $self->routernum
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 );
464 Assign an IP address matching the selected router, and the selected block
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);
479 elsif ( $self->routernum ) {
480 @blocks = $self->router->auto_addr_block;
485 #warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
487 foreach my $block ( @blocks ) {
488 if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
489 # don't change anything
492 $ip_addr = $block->next_free_addr;
494 $self->set(ip_addr => $ip_addr->addr);
495 $self->set(blocknum => $block->blocknum);
499 return 'No IP address available on this router';
504 Assign an address block and router matching the selected IP address.
505 Does nothing if IP address is null.
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);
522 return $self->ip_addr.' is not in an allowed block.';
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';
533 return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
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;
541 # if (my $dup = qsearchs('svc_broadband', {
542 # ip_addr => $self->ip_addr,
543 # svcnum => {op=>'!=', value => $self->svcnum}
545 # return 'IP address conflicts with svcnum '.$dup->svcnum;
550 sub _check_duplicate {
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.
556 @dup = $self->find_duplicates('global', 'ip_addr');
558 return "IP address in use (svcnum ".$dup[0]->svcnum.")";
560 @dup = $self->find_duplicates('global', 'mac_addr');
562 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
571 Returns a NetAddr::IP object containing the IP address of this service. The netmask
578 new NetAddr::IP ($self->ip_addr);
583 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
589 qsearchs('addr_block', { blocknum => $self->blocknum });
594 Returns the FS::router record for this service.
600 qsearchs('router', { routernum => $self->routernum });
603 =item allowed_routers
605 Returns a list of allowed FS::router objects.
609 sub allowed_routers {
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;
626 =item mac_addr_formatted CASE DELIMITER
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
634 sub mac_addr_formatted {
636 my ($case, $delim) = @_;
637 my $addr = $self->mac_addr;
638 $addr = lc($addr) if $case =~ /^l/i;
639 join( $delim || '', $addr =~ /../g );
646 local($FS::svc_Common::noexport_hack) = 1;
648 # set routernum to addr_block.routernum
649 foreach my $self (qsearch('svc_broadband', {
650 blocknum => {op => '!=', value => ''},
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";
659 my $ip_addr = $self->ip_addr;
660 my $routernum = $addr_block->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";
672 $error ||= $self->replace;
673 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
674 ":\n$error; skipped\n"
678 warn "svcnum ".$self->svcnum.
679 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
683 # assign blocknums to services that should have them
684 my @all_blocks = qsearch('addr_block', { });
685 SVC: foreach my $self (
687 'select' => 'svc_broadband.*',
688 'table' => 'svc_broadband',
689 'addl_from' => 'JOIN router USING (routernum)',
691 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
692 'AND router.manual_addr IS NULL',
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"
709 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
721 The business with sb_field has been 'fixed', in a manner of speaking.
723 allowed_routers isn't agent virtualized because part_svc isn't agent
726 Having both routernum and blocknum as foreign keys is somewhat dubious.
730 FS::svc_Common, FS::Record, FS::addr_block,
731 FS::part_svc, schema.html from the base documentation.