1 package FS::svc_broadband;
10 use vars qw(@ISA $conf);
12 { no warnings 'redefine'; use NetAddr::IP; }
13 use FS::Record qw( qsearchs qsearch dbh );
17 use FS::part_svc_router;
20 $FS::UID::callback{'FS::svc_broadband'} = sub {
26 FS::svc_broadband - Object methods for svc_broadband records
30 use FS::svc_broadband;
32 $record = new FS::svc_broadband \%hash;
33 $record = new FS::svc_broadband { 'column' => 'value' };
35 $error = $record->insert;
37 $error = $new_record->replace($old_record);
39 $error = $record->delete;
41 $error = $record->check;
43 $error = $record->suspend;
45 $error = $record->unsuspend;
47 $error = $record->cancel;
51 An FS::svc_broadband object represents a 'broadband' Internet connection, such
52 as a DSL, cable modem, or fixed wireless link. These services are assumed to
53 have the following properties:
55 FS::svc_broadband inherits from FS::svc_Common. The following fields are
60 =item svcnum - primary key
62 =item blocknum - see FS::addr_block
65 speed_up - maximum upload speed, in bits per second. If set to zero, upload
66 speed will be unlimited. Exports that do traffic shaping should handle this
67 correctly, and not blindly set the upload speed to zero and kill the customer's
71 speed_down - maximum download speed, as above
73 =item ip_addr - the customer's IP address. If the customer needs more than one
74 IP address, set this to the address of the customer's router. As a result, the
75 customer's router will have the same address for both its internal and external
76 interfaces thus saving address space. This has been found to work on most NAT
89 Creates a new svc_broadband. To add the record to the database, see
92 Note that this stores the hash reference, not a distinct copy of the hash it
93 points to. You can ask the object for a copy with the I<hash> method.
99 'name' => 'Wireless broadband',
100 'name_plural' => 'Wireless broadband services',
101 'longname_plural' => 'Fixed wireless broadband services',
102 'display_weight' => 50,
103 'cancel_weight' => 70,
104 'ip_field' => 'ip_addr',
106 'svcnum' => 'Service',
107 'description' => 'Descriptive label for this particular device',
108 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
109 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
110 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
112 { 'label' => 'Address block',
114 'select_table' => 'addr_block',
115 'select_key' => 'blocknum',
116 'select_label' => 'cidr',
117 'disable_inventory' => 1,
119 'plan_id' => 'Service Plan Id',
120 'performance_profile' => 'Peformance Profile',
121 'authkey' => 'Authentication key',
122 'mac_addr' => 'MAC address',
123 'latitude' => 'Latitude',
124 'longitude' => 'Longitude',
125 'altitude' => 'Altitude',
126 'vlan_profile' => 'VLAN profile',
127 'sectornum' => 'Tower/sector',
128 'routernum' => 'Router/block',
130 label => 'RADIUS groups',
131 type => 'select-radius_group.html',
132 #select_table => 'radius_group',
133 #select_key => 'groupnum',
134 #select_label => 'groupname',
135 disable_inventory => 1,
142 sub table { 'svc_broadband'; }
144 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
148 Class method which returns a qsearch hash expression to search for parameters
149 specified in HASHREF.
155 =item unlinked - set to search for all unlinked services. Overrides all other options.
165 =item pkgpart - arrayref
167 =item routernum - arrayref
169 =item sectornum - arrayref
171 =item towernum - arrayref
180 my ($class, $params) = @_;
183 'LEFT JOIN cust_svc USING ( svcnum )',
184 'LEFT JOIN part_svc USING ( svcpart )',
185 'LEFT JOIN cust_pkg USING ( pkgnum )',
186 'LEFT JOIN cust_main USING ( custnum )',
189 # based on FS::svc_acct::search, probably the most mature of the bunch
191 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
194 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
195 push @where, "cust_main.agentnum = $1";
197 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
198 'null_right' => 'View/link unlinked services',
199 'table' => 'cust_main'
203 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
204 push @where, "custnum = $1";
207 #pkgpart, now properly untainted, can be arrayref
208 for my $pkgpart ( $params->{'pkgpart'} ) {
209 if ( ref $pkgpart ) {
210 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
211 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
213 elsif ( $pkgpart =~ /^(\d+)$/ ) {
214 push @where, "cust_pkg.pkgpart = $1";
218 #routernum, can be arrayref
219 for my $routernum ( $params->{'routernum'} ) {
220 # this no longer uses addr_block
221 if ( ref $routernum and grep { $_ } @$routernum ) {
222 my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
224 push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
225 push @orwhere, "svc_broadband.routernum IS NULL"
226 if grep /^none$/, @$routernum;
227 push @where, '( '.join(' OR ', @orwhere).' )';
229 elsif ( $routernum =~ /^(\d+)$/ ) {
230 push @where, "svc_broadband.routernum = $1";
232 elsif ( $routernum eq 'none' ) {
233 push @where, "svc_broadband.routernum IS NULL";
237 #sector and tower, as above
238 my @where_sector = $class->tower_sector_sql($params);
239 if ( @where_sector ) {
240 push @where, @where_sector;
241 push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
245 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
246 push @where, "svcnum = $1";
250 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
251 push @where, "svcpart = $1";
255 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
256 push @from, 'LEFT JOIN export_svc USING ( svcpart )';
257 push @where, "exportnum = $1";
261 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
262 push @where, "ip_addr = '$1'";
266 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
267 push @where, "custnum = $1";
270 my $addl_from = join(' ', @from);
272 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
273 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
275 'table' => 'svc_broadband',
277 'select' => join(', ',
281 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
283 'extra_sql' => $extra_sql,
284 'addl_from' => $addl_from,
285 'order_by' => ($params->{'order_by'} || 'ORDER BY svcnum'),
286 'count_query' => $count_query,
290 =item search_sql STRING
292 Class method which returns an SQL fragment to search for the given string.
297 my( $class, $string ) = @_;
298 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
299 $class->search_sql_field('ip_addr', $string );
300 } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
301 $class->search_sql_field('mac_addr', uc($string));
302 } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
304 $class->search_sql_field('mac_addr', uc($string) );
305 } elsif ( $string =~ /^(\d+)$/ ) {
306 my $table = $class->table;
307 "$table.svcnum = $1";
315 Returns the IP address, MAC address and description.
321 my $label = 'IP:'. ($self->ip_addr || 'Unknown');
322 $label .= ', MAC:'. $self->mac_addr
324 $label .= ' ('. $self->description. ')'
325 if $self->description;
329 =item insert [ , OPTION => VALUE ... ]
331 Adds this record to the database. If there is an error, returns the error,
332 otherwise returns false.
334 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
335 defined. An FS::cust_svc record will be created and inserted.
337 Currently available options are: I<depend_jobnum>
339 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
340 jobnums), all provisioning jobs will have a dependancy on the supplied
341 jobnum(s) (they will not run until the specific job(s) complete(s)).
343 # Standard FS::svc_Common::insert
347 Delete this record from the database.
351 # Standard FS::svc_Common::delete
353 =item replace OLD_RECORD
355 Replaces the OLD_RECORD with this one in the database. If there is an error,
356 returns the error, otherwise returns false.
358 # Standard FS::svc_Common::replace
362 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
366 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
370 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
374 Checks all fields to make sure this is a valid broadband service. If there is
375 an error, returns the error, otherwise returns false. Called by the insert
382 my $x = $self->setfixed;
384 return $x unless ref($x);
387 my $mac_addr = uc($self->get('mac_addr'));
388 $mac_addr =~ s/[\W_]//g;
389 $self->set('mac_addr', $mac_addr);
392 $self->ut_numbern('svcnum')
393 || $self->ut_numbern('blocknum')
394 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
395 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
396 || $self->ut_textn('description')
397 || $self->ut_numbern('speed_up')
398 || $self->ut_numbern('speed_down')
399 || $self->ut_ipn('ip_addr')
400 || $self->ut_hexn('mac_addr')
401 || $self->ut_hexn('auth_key')
402 || $self->ut_coordn('latitude')
403 || $self->ut_coordn('longitude')
404 || $self->ut_sfloatn('altitude')
405 || $self->ut_textn('vlan_profile')
406 || $self->ut_textn('plan_id')
408 return $error if $error;
410 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
411 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
413 my $cust_svc = $self->svcnum
414 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
419 $cust_pkg = $cust_svc->cust_pkg;
420 $svcpart = $cust_svc->svcpart;
422 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
423 return "Invalid pkgnum" unless $cust_pkg;
424 $svcpart = $self->svcpart;
426 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
428 if ( $conf->exists('auto_router') and $self->ip_addr and !$self->routernum ) {
429 # assign_router is guaranteed to provide a router that's legal
430 # for this agent and svcpart
431 my $error = $self->_check_ip_addr || $self->assign_router;
432 return $error if $error;
434 elsif ($self->routernum) {
435 return "Router ".$self->routernum." does not provide this service"
436 unless qsearchs('part_svc_router', {
438 routernum => $self->routernum
441 my $router = $self->router;
442 return "Router ".$self->routernum." does not serve this customer"
443 if $router->agentnum and $agentnum and $router->agentnum != $agentnum;
445 if ( $router->manual_addr ) {
449 my $addr_block = $self->addr_block;
450 if ( $self->ip_addr eq ''
451 and not ( $addr_block and $addr_block->manual_flag ) ) {
452 my $error = $self->assign_ip_addr;
453 return $error if $error;
457 my $error = $self->_check_ip_addr;
458 return $error if $error;
459 } # if $self->routernum
461 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
462 my $l = $cust_pkg->cust_location_or_main;
463 if ( $l->ship_latitude && $l->ship_longitude ) {
464 $self->latitude( $l->ship_latitude );
465 $self->longitude( $l->ship_longitude );
466 } elsif ( $l->latitude && $l->longitude ) {
467 $self->latitude( $l->latitude );
468 $self->longitude( $l->longitude );
477 Assign an IP address matching the selected router, and the selected block
487 if ( $self->addr_block and $self->addr_block->routernum == $self->routernum ) {
488 # simple case: user chose a block, find an address in that block
489 # (this overrides an existing IP address if it's not in the block)
490 @blocks = ($self->addr_block);
492 elsif ( $self->routernum ) {
493 @blocks = $self->router->auto_addr_block;
498 #warn "assigning ip address in blocks\n".join("\n",map{$_->cidr} @blocks)."\n";
500 foreach my $block ( @blocks ) {
501 if ( $self->ip_addr and $block->NetAddr->contains($self->NetAddr) ) {
502 # don't change anything
505 $ip_addr = $block->next_free_addr;
507 $self->set(ip_addr => $ip_addr->addr);
508 $self->set(blocknum => $block->blocknum);
512 return 'No IP address available on this router';
517 Assign an address block and router matching the selected IP address.
518 Does nothing if IP address is null.
524 return '' if !$self->ip_addr;
525 #warn "assigning router/block for ".$self->ip_addr."\n";
526 foreach my $router ($self->allowed_routers) {
527 foreach my $block ($router->addr_block) {
528 if ( $block->NetAddr->contains($self->NetAddr) ) {
529 $self->blocknum($block->blocknum);
530 $self->routernum($block->routernum);
535 return $self->ip_addr.' is not in an allowed block.';
541 if (not($self->ip_addr) or $self->ip_addr eq '0.0.0.0') {
542 return '' if $conf->exists('svc_broadband-allow_null_ip_addr');
543 return 'IP address required';
546 return 'Cannot parse address: '.$self->ip_addr unless $self->NetAddr;
549 if ( $self->addr_block
550 and not $self->addr_block->NetAddr->contains($self->NetAddr) ) {
551 return 'Address '.$self->ip_addr.' not in block '.$self->addr_block->cidr;
554 # if (my $dup = qsearchs('svc_broadband', {
555 # ip_addr => $self->ip_addr,
556 # svcnum => {op=>'!=', value => $self->svcnum}
558 # return 'IP address conflicts with svcnum '.$dup->svcnum;
563 sub _check_duplicate {
565 # Not a reliable check because the table isn't locked, but
566 # that's why we have a unique index. This is just to give a
567 # friendlier error message.
569 @dup = $self->find_duplicates('global', 'ip_addr');
571 return "IP address in use (svcnum ".$dup[0]->svcnum.")";
573 @dup = $self->find_duplicates('global', 'mac_addr');
575 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
584 Returns a NetAddr::IP object containing the IP address of this service. The netmask
591 new NetAddr::IP ($self->ip_addr);
596 Returns the FS::addr_block record (i.e. the address block) for this broadband service.
602 qsearchs('addr_block', { blocknum => $self->blocknum });
607 Returns the FS::router record for this service.
613 qsearchs('router', { routernum => $self->routernum });
616 =item allowed_routers
618 Returns a list of allowed FS::router objects.
622 sub allowed_routers {
624 my $svcpart = $self->svcnum ? $self->cust_svc->svcpart : $self->svcpart;
625 my @r = map { $_->router } qsearch('part_svc_router',
626 { svcpart => $svcpart });
627 if ( $self->cust_main ) {
628 my $agentnum = $self->cust_main->agentnum;
629 return grep { !$_->agentnum or $_->agentnum == $agentnum } @r;
640 local($FS::svc_Common::noexport_hack) = 1;
642 # set routernum to addr_block.routernum
643 foreach my $self (qsearch('svc_broadband', {
644 blocknum => {op => '!=', value => ''},
647 my $addr_block = $self->addr_block;
648 if ( !$addr_block ) {
649 # super paranoid mode
650 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
653 my $ip_addr = $self->ip_addr;
654 my $routernum = $addr_block->routernum;
656 $self->set(routernum => $routernum);
657 my $error = $self->check;
658 # sanity check: don't allow this to change IP address or block
659 # (other than setting blocknum to null for a non-auto-assigned router)
660 if ( $self->ip_addr ne $ip_addr
661 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
662 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
666 $error ||= $self->replace;
667 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
668 ":\n$error; skipped\n"
672 warn "svcnum ".$self->svcnum.
673 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
677 # assign blocknums to services that should have them
678 my @all_blocks = qsearch('addr_block', { });
679 SVC: foreach my $self (
681 'select' => 'svc_broadband.*',
682 'table' => 'svc_broadband',
683 'addl_from' => 'JOIN router USING (routernum)',
685 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
686 'AND router.manual_addr IS NULL',
690 next SVC if $self->ip_addr eq '';
691 my $NetAddr = $self->NetAddr;
692 # inefficient, but should only need to run once
693 foreach my $block (@all_blocks) {
694 if ($block->NetAddr->contains($NetAddr)) {
695 $self->set(blocknum => $block->blocknum);
696 my $error = $self->replace;
697 warn "WARNING: error assigning blocknum ".$block->blocknum.
698 " to service ".$self->svcnum."\n$error; skipped\n"
703 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
715 The business with sb_field has been 'fixed', in a manner of speaking.
717 allowed_routers isn't agent virtualized because part_svc isn't agent
720 Having both routernum and blocknum as foreign keys is somewhat dubious.
724 FS::svc_Common, FS::Record, FS::addr_block,
725 FS::part_svc, schema.html from the base documentation.