1 package FS::svc_broadband;
14 { no warnings 'redefine'; use NetAddr::IP; }
15 use FS::Record qw( qsearchs qsearch dbh );
18 use FS::part_svc_router;
21 $FS::UID::callback{'FS::svc_broadband'} = sub {
27 FS::svc_broadband - Object methods for svc_broadband records
31 use FS::svc_broadband;
33 $record = new FS::svc_broadband \%hash;
34 $record = new FS::svc_broadband { 'column' => 'value' };
36 $error = $record->insert;
38 $error = $new_record->replace($old_record);
40 $error = $record->delete;
42 $error = $record->check;
44 $error = $record->suspend;
46 $error = $record->unsuspend;
48 $error = $record->cancel;
52 An FS::svc_broadband object represents a 'broadband' Internet connection, such
53 as a DSL, cable modem, or fixed wireless link. These services are assumed to
54 have the following properties:
56 FS::svc_broadband inherits from FS::svc_Common. The following fields are
61 =item svcnum - primary key
63 =item blocknum - see FS::addr_block
66 speed_up - maximum upload speed, in bits per second. If set to zero, upload
67 speed will be unlimited. Exports that do traffic shaping should handle this
68 correctly, and not blindly set the upload speed to zero and kill the customer's
72 speed_down - maximum download speed, as above
74 =item ip_addr - the customer's IP address. If the customer needs more than one
75 IP address, set this to the address of the customer's router. As a result, the
76 customer's router will have the same address for both its internal and external
77 interfaces thus saving address space. This has been found to work on most NAT
90 Creates a new svc_broadband. To add the record to the database, see
93 Note that this stores the hash reference, not a distinct copy of the hash it
94 points to. You can ask the object for a copy with the I<hash> method.
100 'name' => 'Wireless broadband',
101 'name_plural' => 'Wireless broadband services',
102 'longname_plural' => 'Fixed wireless broadband services',
103 'display_weight' => 50,
104 'cancel_weight' => 70,
105 'ip_field' => 'ip_addr',
106 'manual_require' => 1,
108 'svcnum' => 'Service',
109 'description' => 'Descriptive label',
110 'speed_down' => 'Download speed (Kbps)',
111 'speed_up' => 'Upload speed (Kbps)',
112 'ip_addr' => 'IP address',
114 'label' => 'Address block',
116 'select_table' => 'addr_block',
117 'select_key' => 'blocknum',
118 'select_label' => 'cidr',
119 'disable_inventory' => 1,
121 'plan_id' => 'Service Plan Id',
122 'performance_profile' => 'Peformance Profile',
123 'authkey' => 'Authentication key',
124 'mac_addr' => 'MAC address',
125 'latitude' => 'Latitude',
126 'longitude' => 'Longitude',
127 'altitude' => 'Altitude',
128 'vlan_profile' => 'VLAN profile',
129 'sectornum' => 'Tower/sector',
130 'routernum' => 'Router/block',
132 label => 'RADIUS groups',
133 type => 'select-radius_group.html',
134 #select_table => 'radius_group',
135 #select_key => 'groupnum',
136 #select_label => 'groupname',
138 disable_inventory => 1,
141 'radio_serialnum' => 'Radio Serial Number',
142 'radio_location' => 'Radio Location',
143 'poe_location' => 'POE Location',
146 'shared_svcnum' => { label => 'Shared Service',
147 type => 'search-svc_broadband',
148 disable_inventory => 1,
150 'serviceid' => 'Torrus serviceid', #but is should be hidden
155 sub table { 'svc_broadband'; }
157 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
161 Class method which returns a qsearch hash expression to search for parameters
162 specified in HASHREF.
168 =item unlinked - set to search for all unlinked services. Overrides all other options.
178 =item pkgpart - arrayref
180 =item routernum - arrayref
182 =item sectornum - arrayref
184 =item towernum - arrayref
193 my( $class, $params, $from, $where ) = @_;
195 #routernum, can be arrayref
196 for my $routernum ( $params->{'routernum'} ) {
197 # this no longer uses addr_block
198 if ( ref $routernum and grep { $_ } @$routernum ) {
199 my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
201 push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
202 push @orwhere, "svc_broadband.routernum IS NULL"
203 if grep /^none$/, @$routernum;
204 push @$where, '( '.join(' OR ', @orwhere).' )';
206 elsif ( $routernum =~ /^(\d+)$/ ) {
207 push @$where, "svc_broadband.routernum = $1";
209 elsif ( $routernum eq 'none' ) {
210 push @$where, "svc_broadband.routernum IS NULL";
214 #this should probably move to svc_Tower_Mixin, or maybe we never should have
215 # done svc_acct # towers (or, as mark thought, never should have done
218 #sector and tower, as above
219 my @where_sector = $class->tower_sector_sql($params);
220 if ( @where_sector ) {
221 push @$where, @where_sector;
222 push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
226 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
227 push @$where, "ip_addr = '$1'";
232 =item search_sql STRING
234 Class method which returns an SQL fragment to search for the given string.
239 my( $class, $string ) = @_;
240 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
241 $class->search_sql_field('ip_addr', $string );
242 } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
243 $class->search_sql_field('mac_addr', uc($string));
244 } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
246 $class->search_sql_field('mac_addr', uc($string) );
247 } elsif ( $string =~ /^(\d+)$/ ) {
248 my $table = $class->table;
249 "$table.svcnum = $1";
255 =item smart_search STRING
260 my( $class, $string ) = @_;
262 'table' => $class->table, #'svc_broadband',
264 'extra_sql' => 'WHERE '. $class->search_sql($string),
270 Returns the IP address, MAC address and description.
276 my $label = 'IP:'. ($self->ip_addr || 'Unknown');
277 $label .= ', MAC:'. $self->mac_addr
279 $label .= ' ('. $self->description. ')'
280 if $self->description;
284 =item insert [ , OPTION => VALUE ... ]
286 Adds this record to the database. If there is an error, returns the error,
287 otherwise returns false.
289 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
290 defined. An FS::cust_svc record will be created and inserted.
292 Currently available options are: I<depend_jobnum>
294 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
295 jobnums), all provisioning jobs will have a dependancy on the supplied
296 jobnum(s) (they will not run until the specific job(s) complete(s)).
300 # Standard FS::svc_Common::insert
304 Delete this record from the database.
308 # Standard FS::svc_Common::delete
310 =item replace OLD_RECORD
312 Replaces the OLD_RECORD with this one in the database. If there is an error,
313 returns the error, otherwise returns false.
315 # Standard FS::svc_Common::replace
319 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
323 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
327 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
331 Checks all fields to make sure this is a valid broadband service. If there is
332 an error, returns the error, otherwise returns false. Called by the insert
339 my $x = $self->setfixed;
341 return $x unless ref($x);
344 my $mac_addr = uc($self->get('mac_addr'));
345 $mac_addr =~ s/[\W_]//g;
346 $self->set('mac_addr', $mac_addr);
349 $self->ut_numbern('svcnum')
350 || $self->ut_numbern('blocknum')
351 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
352 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
353 || $self->ut_textn('description')
354 || $self->ut_numbern('speed_up')
355 || $self->ut_numbern('speed_down')
356 || $self->ut_ipn('ip_addr')
357 || $self->ut_hexn('mac_addr')
358 || $self->ut_hexn('auth_key')
359 || $self->ut_coordn('latitude')
360 || $self->ut_coordn('longitude')
361 || $self->ut_sfloatn('altitude')
362 || $self->ut_textn('vlan_profile')
363 || $self->ut_textn('plan_id')
364 || $self->ut_alphan('radio_serialnum')
365 || $self->ut_textn('radio_location')
366 || $self->ut_textn('poe_location')
367 || $self->ut_snumbern('rssi')
368 || $self->ut_numbern('suid')
369 || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
370 || $self->ut_textn('serviceid') #too lenient?
372 return $error if $error;
374 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
375 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
377 my $cust_svc = $self->svcnum
378 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
383 $cust_pkg = $cust_svc->cust_pkg;
384 $svcpart = $cust_svc->svcpart;
386 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
387 return "Invalid pkgnum" unless $cust_pkg;
388 $svcpart = $self->svcpart;
390 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
392 # assign IP address / router / block
393 $error = $self->svc_ip_check;
394 return $error if $error;
396 and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
397 return 'IP address is required';
400 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
401 my $l = $cust_pkg->cust_location_or_main;
402 if ( $l->ship_latitude && $l->ship_longitude ) {
403 $self->latitude( $l->ship_latitude );
404 $self->longitude( $l->ship_longitude );
405 } elsif ( $l->latitude && $l->longitude ) {
406 $self->latitude( $l->latitude );
407 $self->longitude( $l->longitude );
414 sub _check_duplicate {
416 # Not a reliable check because the table isn't locked, but
417 # that's why we have a unique index. This is just to give a
418 # friendlier error message.
420 @dup = $self->find_duplicates('global', 'mac_addr');
422 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
432 local($FS::svc_Common::noexport_hack) = 1;
434 # fix wrong-case MAC addresses
436 $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
439 # set routernum to addr_block.routernum
440 foreach my $self (qsearch('svc_broadband', {
441 blocknum => {op => '!=', value => ''},
444 my $addr_block = $self->addr_block;
445 if ( !$addr_block ) {
446 # super paranoid mode
447 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
450 my $ip_addr = $self->ip_addr;
451 my $routernum = $addr_block->routernum;
453 $self->set(routernum => $routernum);
454 my $error = $self->check;
455 # sanity check: don't allow this to change IP address or block
456 # (other than setting blocknum to null for a non-auto-assigned router)
457 if ( $self->ip_addr ne $ip_addr
458 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
459 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
463 $error ||= $self->replace;
464 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
465 ":\n$error; skipped\n"
469 warn "svcnum ".$self->svcnum.
470 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
474 # assign blocknums to services that should have them
475 my @all_blocks = qsearch('addr_block', { });
476 SVC: foreach my $self (
478 'select' => 'svc_broadband.*',
479 'table' => 'svc_broadband',
480 'addl_from' => 'JOIN router USING (routernum)',
482 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
483 'AND router.manual_addr IS NULL',
487 next SVC if $self->ip_addr eq '';
488 my $NetAddr = $self->NetAddr;
489 # inefficient, but should only need to run once
490 foreach my $block (@all_blocks) {
491 if ($block->NetAddr->contains($NetAddr)) {
492 $self->set(blocknum => $block->blocknum);
493 my $error = $self->replace;
494 warn "WARNING: error assigning blocknum ".$block->blocknum.
495 " to service ".$self->svcnum."\n$error; skipped\n"
500 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
505 require FS::Misc::FixIPFormat;
506 FS::Misc::FixIPFormat::fix_bad_addresses_in_table(
507 'svc_broadband', 'svcnum', 'ip_addr',
517 The business with sb_field has been 'fixed', in a manner of speaking.
519 allowed_routers isn't agent virtualized because part_svc isn't agent
522 Having both routernum and blocknum as foreign keys is somewhat dubious.
526 FS::svc_Common, FS::Record, FS::addr_block,
527 FS::part_svc, schema.html from the base documentation.