1 package FS::svc_broadband;
12 { no warnings 'redefine'; use NetAddr::IP; }
13 use FS::Record qw( qsearchs qsearch dbh );
16 use FS::part_svc_router;
19 $FS::UID::callback{'FS::svc_broadband'} = sub {
25 FS::svc_broadband - Object methods for svc_broadband records
29 use FS::svc_broadband;
31 $record = new FS::svc_broadband \%hash;
32 $record = new FS::svc_broadband { 'column' => 'value' };
34 $error = $record->insert;
36 $error = $new_record->replace($old_record);
38 $error = $record->delete;
40 $error = $record->check;
42 $error = $record->suspend;
44 $error = $record->unsuspend;
46 $error = $record->cancel;
50 An FS::svc_broadband object represents a 'broadband' Internet connection, such
51 as a DSL, cable modem, or fixed wireless link. These services are assumed to
52 have the following properties:
54 FS::svc_broadband inherits from FS::svc_Common. The following fields are
59 =item svcnum - primary key
61 =item blocknum - see FS::addr_block
64 speed_up - maximum upload speed, in bits per second. If set to zero, upload
65 speed will be unlimited. Exports that do traffic shaping should handle this
66 correctly, and not blindly set the upload speed to zero and kill the customer's
70 speed_down - maximum download speed, as above
72 =item ip_addr - the customer's IP address. If the customer needs more than one
73 IP address, set this to the address of the customer's router. As a result, the
74 customer's router will have the same address for both its internal and external
75 interfaces thus saving address space. This has been found to work on most NAT
88 Creates a new svc_broadband. To add the record to the database, see
91 Note that this stores the hash reference, not a distinct copy of the hash it
92 points to. You can ask the object for a copy with the I<hash> method.
98 'name' => 'Wireless broadband',
99 'name_plural' => 'Wireless broadband services',
100 'longname_plural' => 'Fixed wireless broadband services',
101 'display_weight' => 50,
102 'cancel_weight' => 70,
103 'ip_field' => 'ip_addr',
105 'svcnum' => 'Service',
106 'description' => 'Descriptive label for this particular device',
107 'speed_down' => 'Maximum download speed for this service in Kbps. 0 denotes unlimited.',
108 'speed_up' => 'Maximum upload speed for this service in Kbps. 0 denotes unlimited.',
109 'ip_addr' => 'IP address. Leave blank for automatic assignment.',
111 { 'label' => 'Address block',
113 'select_table' => 'addr_block',
114 'select_key' => 'blocknum',
115 'select_label' => 'cidr',
116 'disable_inventory' => 1,
118 'plan_id' => 'Service Plan Id',
119 'performance_profile' => 'Peformance Profile',
120 'authkey' => 'Authentication key',
121 'mac_addr' => 'MAC address',
122 'latitude' => 'Latitude',
123 'longitude' => 'Longitude',
124 'altitude' => 'Altitude',
125 'vlan_profile' => 'VLAN profile',
126 'sectornum' => 'Tower/sector',
127 'routernum' => 'Router/block',
129 label => 'RADIUS groups',
130 type => 'select-radius_group.html',
131 #select_table => 'radius_group',
132 #select_key => 'groupnum',
133 #select_label => 'groupname',
134 disable_inventory => 1,
141 sub table { 'svc_broadband'; }
143 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
147 Class method which returns a qsearch hash expression to search for parameters
148 specified in HASHREF.
154 =item unlinked - set to search for all unlinked services. Overrides all other options.
164 =item pkgpart - arrayref
166 =item routernum - arrayref
168 =item sectornum - arrayref
170 =item towernum - arrayref
179 my ($class, $params) = @_;
182 'LEFT JOIN cust_svc USING ( svcnum )',
183 'LEFT JOIN part_svc USING ( svcpart )',
184 'LEFT JOIN cust_pkg USING ( pkgnum )',
185 'LEFT JOIN cust_main USING ( custnum )',
188 # based on FS::svc_acct::search, probably the most mature of the bunch
190 push @where, 'pkgnum IS NULL' if $params->{'unlinked'};
193 if ( $params->{'agentnum'} =~ /^(\d+)$/ and $1 ) {
194 push @where, "cust_main.agentnum = $1";
196 push @where, $FS::CurrentUser::CurrentUser->agentnums_sql(
197 'null_right' => 'View/link unlinked services',
198 'table' => 'cust_main'
202 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) {
203 push @where, "custnum = $1";
206 #pkgpart, now properly untainted, can be arrayref
207 for my $pkgpart ( $params->{'pkgpart'} ) {
208 if ( ref $pkgpart ) {
209 my $where = join(',', map { /^(\d+)$/ ? $1 : () } @$pkgpart );
210 push @where, "cust_pkg.pkgpart IN ($where)" if $where;
212 elsif ( $pkgpart =~ /^(\d+)$/ ) {
213 push @where, "cust_pkg.pkgpart = $1";
217 #routernum, can be arrayref
218 for my $routernum ( $params->{'routernum'} ) {
219 # this no longer uses addr_block
220 if ( ref $routernum and grep { $_ } @$routernum ) {
221 my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
223 push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
224 push @orwhere, "svc_broadband.routernum IS NULL"
225 if grep /^none$/, @$routernum;
226 push @where, '( '.join(' OR ', @orwhere).' )';
228 elsif ( $routernum =~ /^(\d+)$/ ) {
229 push @where, "svc_broadband.routernum = $1";
231 elsif ( $routernum eq 'none' ) {
232 push @where, "svc_broadband.routernum IS NULL";
236 #sector and tower, as above
237 my @where_sector = $class->tower_sector_sql($params);
238 if ( @where_sector ) {
239 push @where, @where_sector;
240 push @from, 'LEFT JOIN tower_sector USING ( sectornum )';
244 if ( $params->{'svcnum'} =~ /^(\d+)$/ ) {
245 push @where, "svcnum = $1";
249 if ( $params->{'svcpart'} =~ /^(\d+)$/ ) {
250 push @where, "svcpart = $1";
254 if ( $params->{'exportnum'} =~ /^(\d+)$/ ) {
255 push @from, 'LEFT JOIN export_svc USING ( svcpart )';
256 push @where, "exportnum = $1";
260 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
261 push @where, "ip_addr = '$1'";
265 if ( $params->{'custnum'} =~ /^(\d+)$/ and $1) {
266 push @where, "custnum = $1";
269 my $addl_from = join(' ', @from);
271 $extra_sql = 'WHERE '.join(' AND ', @where) if @where;
272 my $count_query = "SELECT COUNT(*) FROM svc_broadband $addl_from $extra_sql";
274 'table' => 'svc_broadband',
276 'select' => join(', ',
280 FS::UI::Web::cust_sql_fields($params->{'cust_fields'}),
282 'extra_sql' => $extra_sql,
283 'addl_from' => $addl_from,
284 'order_by' => "ORDER BY ".($params->{'order_by'} || 'svcnum'),
285 'count_query' => $count_query,
289 =item search_sql STRING
291 Class method which returns an SQL fragment to search for the given string.
296 my( $class, $string ) = @_;
297 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
298 $class->search_sql_field('ip_addr', $string );
299 }elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
300 $class->search_sql_field('mac_addr', uc($string));
301 }elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
302 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
310 Returns the IP address.
319 =item insert [ , OPTION => VALUE ... ]
321 Adds this record to the database. If there is an error, returns the error,
322 otherwise returns false.
324 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
325 defined. An FS::cust_svc record will be created and inserted.
327 Currently available options are: I<depend_jobnum>
329 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
330 jobnums), all provisioning jobs will have a dependancy on the supplied
331 jobnum(s) (they will not run until the specific job(s) complete(s)).
335 # Standard FS::svc_Common::insert
339 Delete this record from the database.
343 # Standard FS::svc_Common::delete
345 =item replace OLD_RECORD
347 Replaces the OLD_RECORD with this one in the database. If there is an error,
348 returns the error, otherwise returns false.
350 # Standard FS::svc_Common::replace
354 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
358 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
362 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
366 Checks all fields to make sure this is a valid broadband service. If there is
367 an error, returns the error, otherwise returns false. Called by the insert
374 my $x = $self->setfixed;
376 return $x unless ref($x);
379 my $mac_addr = uc($self->get('mac_addr'));
380 $mac_addr =~ s/[-: ]//g;
381 $self->set('mac_addr', $mac_addr);
384 $self->ut_numbern('svcnum')
385 || $self->ut_numbern('blocknum')
386 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
387 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
388 || $self->ut_textn('description')
389 || $self->ut_numbern('speed_up')
390 || $self->ut_numbern('speed_down')
391 || $self->ut_ipn('ip_addr')
392 || $self->ut_hexn('mac_addr')
393 || $self->ut_hexn('auth_key')
394 || $self->ut_coordn('latitude')
395 || $self->ut_coordn('longitude')
396 || $self->ut_sfloatn('altitude')
397 || $self->ut_textn('vlan_profile')
398 || $self->ut_textn('plan_id')
400 return $error if $error;
402 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
403 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
405 my $cust_svc = $self->svcnum
406 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
411 $cust_pkg = $cust_svc->cust_pkg;
412 $svcpart = $cust_svc->svcpart;
414 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
415 return "Invalid pkgnum" unless $cust_pkg;
416 $svcpart = $self->svcpart;
418 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
420 # assign IP address / router / block
421 $error = $self->svc_ip_check;
422 return $error if $error;
424 and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
425 return 'IP address is required';
428 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
429 my $l = $cust_pkg->cust_location_or_main;
430 if ( $l->ship_latitude && $l->ship_longitude ) {
431 $self->latitude( $l->ship_latitude );
432 $self->longitude( $l->ship_longitude );
433 } elsif ( $l->latitude && $l->longitude ) {
434 $self->latitude( $l->latitude );
435 $self->longitude( $l->longitude );
442 sub _check_duplicate {
444 # Not a reliable check because the table isn't locked, but
445 # that's why we have a unique index. This is just to give a
446 # friendlier error message.
448 @dup = $self->find_duplicates('global', 'mac_addr');
450 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
456 =item mac_addr_formatted CASE DELIMITER
458 Format the MAC address (for use by exports). If CASE starts with "l"
459 (for "lowercase"), it's returned in lowercase. DELIMITER is inserted
464 sub mac_addr_formatted {
466 my ($case, $delim) = @_;
467 my $addr = $self->mac_addr;
468 $addr = lc($addr) if $case =~ /^l/i;
469 join( $delim || '', $addr =~ /../g );
476 local($FS::svc_Common::noexport_hack) = 1;
478 # set routernum to addr_block.routernum
479 foreach my $self (qsearch('svc_broadband', {
480 blocknum => {op => '!=', value => ''},
483 my $addr_block = $self->addr_block;
484 if ( !$addr_block ) {
485 # super paranoid mode
486 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
489 my $ip_addr = $self->ip_addr;
490 my $routernum = $addr_block->routernum;
492 $self->set(routernum => $routernum);
493 my $error = $self->check;
494 # sanity check: don't allow this to change IP address or block
495 # (other than setting blocknum to null for a non-auto-assigned router)
496 if ( $self->ip_addr ne $ip_addr
497 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
498 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
502 $error ||= $self->replace;
503 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
504 ":\n$error; skipped\n"
508 warn "svcnum ".$self->svcnum.
509 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
513 # assign blocknums to services that should have them
514 my @all_blocks = qsearch('addr_block', { });
515 SVC: foreach my $self (
517 'select' => 'svc_broadband.*',
518 'table' => 'svc_broadband',
519 'addl_from' => 'JOIN router USING (routernum)',
521 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
522 'AND router.manual_addr IS NULL',
526 next SVC if $self->ip_addr eq '';
527 my $NetAddr = $self->NetAddr;
528 # inefficient, but should only need to run once
529 foreach my $block (@all_blocks) {
530 if ($block->NetAddr->contains($NetAddr)) {
531 $self->set(blocknum => $block->blocknum);
532 my $error = $self->replace;
533 warn "WARNING: error assigning blocknum ".$block->blocknum.
534 " to service ".$self->svcnum."\n$error; skipped\n"
539 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
551 The business with sb_field has been 'fixed', in a manner of speaking.
553 allowed_routers isn't agent virtualized because part_svc isn't agent
556 Having both routernum and blocknum as foreign keys is somewhat dubious.
560 FS::svc_Common, FS::Record, FS::addr_block,
561 FS::part_svc, schema.html from the base documentation.