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',
137 disable_inventory => 1,
140 'radio_serialnum' => 'Radio Serial Number',
141 'radio_location' => 'Radio Location',
142 'poe_location' => 'POE Location',
145 'shared_svcnum' => { label => 'Shared Service',
146 type => 'search-svc_broadband',
147 disable_inventory => 1,
149 'serviceid' => 'Torrus serviceid', #but is should be hidden
154 sub table { 'svc_broadband'; }
156 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
160 Class method which returns a qsearch hash expression to search for parameters
161 specified in HASHREF.
167 =item unlinked - set to search for all unlinked services. Overrides all other options.
177 =item pkgpart - arrayref
179 =item routernum - arrayref
181 =item sectornum - arrayref
183 =item towernum - arrayref
192 my( $class, $params, $from, $where ) = @_;
194 #routernum, can be arrayref
195 for my $routernum ( $params->{'routernum'} ) {
196 # this no longer uses addr_block
197 if ( ref $routernum and grep { $_ } @$routernum ) {
198 my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
200 push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
201 push @orwhere, "svc_broadband.routernum IS NULL"
202 if grep /^none$/, @$routernum;
203 push @$where, '( '.join(' OR ', @orwhere).' )';
205 elsif ( $routernum =~ /^(\d+)$/ ) {
206 push @$where, "svc_broadband.routernum = $1";
208 elsif ( $routernum eq 'none' ) {
209 push @$where, "svc_broadband.routernum IS NULL";
213 #this should probably move to svc_Tower_Mixin, or maybe we never should have
214 # done svc_acct # towers (or, as mark thought, never should have done
217 #sector and tower, as above
218 my @where_sector = $class->tower_sector_sql($params);
219 if ( @where_sector ) {
220 push @$where, @where_sector;
221 push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
225 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
226 push @$where, "ip_addr = '$1'";
231 =item search_sql STRING
233 Class method which returns an SQL fragment to search for the given string.
238 my( $class, $string ) = @_;
239 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
240 $class->search_sql_field('ip_addr', $string );
241 } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
242 $class->search_sql_field('mac_addr', uc($string));
243 } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
245 $class->search_sql_field('mac_addr', uc($string) );
246 } elsif ( $string =~ /^(\d+)$/ ) {
247 my $table = $class->table;
248 "$table.svcnum = $1";
254 =item smart_search STRING
259 my( $class, $string ) = @_;
261 'table' => $class->table, #'svc_broadband',
263 'extra_sql' => 'WHERE '. $class->search_sql($string),
269 Returns the IP address, MAC address and description.
275 my $label = 'IP:'. ($self->ip_addr || 'Unknown');
276 $label .= ', MAC:'. $self->mac_addr
278 $label .= ' ('. $self->description. ')'
279 if $self->description;
283 =item insert [ , OPTION => VALUE ... ]
285 Adds this record to the database. If there is an error, returns the error,
286 otherwise returns false.
288 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
289 defined. An FS::cust_svc record will be created and inserted.
291 Currently available options are: I<depend_jobnum>
293 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
294 jobnums), all provisioning jobs will have a dependancy on the supplied
295 jobnum(s) (they will not run until the specific job(s) complete(s)).
299 # Standard FS::svc_Common::insert
303 Delete this record from the database.
307 # Standard FS::svc_Common::delete
309 =item replace OLD_RECORD
311 Replaces the OLD_RECORD with this one in the database. If there is an error,
312 returns the error, otherwise returns false.
314 # Standard FS::svc_Common::replace
318 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
322 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
326 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
330 Checks all fields to make sure this is a valid broadband service. If there is
331 an error, returns the error, otherwise returns false. Called by the insert
338 my $x = $self->setfixed;
340 return $x unless ref($x);
343 my $mac_addr = uc($self->get('mac_addr'));
344 $mac_addr =~ s/[\W_]//g;
345 $self->set('mac_addr', $mac_addr);
348 $self->ut_numbern('svcnum')
349 || $self->ut_numbern('blocknum')
350 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
351 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
352 || $self->ut_textn('description')
353 || $self->ut_numbern('speed_up')
354 || $self->ut_numbern('speed_down')
355 || $self->ut_ipn('ip_addr')
356 || $self->ut_hexn('mac_addr')
357 || $self->ut_hexn('auth_key')
358 || $self->ut_coordn('latitude')
359 || $self->ut_coordn('longitude')
360 || $self->ut_sfloatn('altitude')
361 || $self->ut_textn('vlan_profile')
362 || $self->ut_textn('plan_id')
363 || $self->ut_alphan('radio_serialnum')
364 || $self->ut_textn('radio_location')
365 || $self->ut_textn('poe_location')
366 || $self->ut_snumbern('rssi')
367 || $self->ut_numbern('suid')
368 || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
369 || $self->ut_textn('serviceid') #too lenient?
371 return $error if $error;
373 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
374 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
376 my $cust_svc = $self->svcnum
377 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
382 $cust_pkg = $cust_svc->cust_pkg;
383 $svcpart = $cust_svc->svcpart;
385 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
386 return "Invalid pkgnum" unless $cust_pkg;
387 $svcpart = $self->svcpart;
389 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
391 # assign IP address / router / block
392 $error = $self->svc_ip_check;
393 return $error if $error;
395 and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
396 return 'IP address is required';
399 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
400 my $l = $cust_pkg->cust_location_or_main;
401 if ( $l->ship_latitude && $l->ship_longitude ) {
402 $self->latitude( $l->ship_latitude );
403 $self->longitude( $l->ship_longitude );
404 } elsif ( $l->latitude && $l->longitude ) {
405 $self->latitude( $l->latitude );
406 $self->longitude( $l->longitude );
413 sub _check_duplicate {
415 # Not a reliable check because the table isn't locked, but
416 # that's why we have a unique index. This is just to give a
417 # friendlier error message.
419 @dup = $self->find_duplicates('global', 'mac_addr');
421 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
431 local($FS::svc_Common::noexport_hack) = 1;
433 # fix wrong-case MAC addresses
435 $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
438 # set routernum to addr_block.routernum
439 foreach my $self (qsearch('svc_broadband', {
440 blocknum => {op => '!=', value => ''},
443 my $addr_block = $self->addr_block;
444 if ( !$addr_block ) {
445 # super paranoid mode
446 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
449 my $ip_addr = $self->ip_addr;
450 my $routernum = $addr_block->routernum;
452 $self->set(routernum => $routernum);
453 my $error = $self->check;
454 # sanity check: don't allow this to change IP address or block
455 # (other than setting blocknum to null for a non-auto-assigned router)
456 if ( $self->ip_addr ne $ip_addr
457 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
458 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
462 $error ||= $self->replace;
463 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
464 ":\n$error; skipped\n"
468 warn "svcnum ".$self->svcnum.
469 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
473 # assign blocknums to services that should have them
474 my @all_blocks = qsearch('addr_block', { });
475 SVC: foreach my $self (
477 'select' => 'svc_broadband.*',
478 'table' => 'svc_broadband',
479 'addl_from' => 'JOIN router USING (routernum)',
481 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
482 'AND router.manual_addr IS NULL',
486 next SVC if $self->ip_addr eq '';
487 my $NetAddr = $self->NetAddr;
488 # inefficient, but should only need to run once
489 foreach my $block (@all_blocks) {
490 if ($block->NetAddr->contains($NetAddr)) {
491 $self->set(blocknum => $block->blocknum);
492 my $error = $self->replace;
493 warn "WARNING: error assigning blocknum ".$block->blocknum.
494 " to service ".$self->svcnum."\n$error; skipped\n"
499 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
511 The business with sb_field has been 'fixed', in a manner of speaking.
513 allowed_routers isn't agent virtualized because part_svc isn't agent
516 Having both routernum and blocknum as foreign keys is somewhat dubious.
520 FS::svc_Common, FS::Record, FS::addr_block,
521 FS::part_svc, schema.html from the base documentation.