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',
107 'speed_down' => 'Download speed (Kbps)',
108 'speed_up' => 'Upload speed (Kbps)',
109 'ip_addr' => 'IP address',
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,
137 'radio_serialnum' => 'Radio Serial Number',
138 'radio_location' => 'Radio Location',
139 'poe_location' => 'POE Location',
142 'shared_svcnum' => { label => 'Shared Service',
143 type => 'search-svc_broadband',
144 disable_inventory => 1,
150 sub table { 'svc_broadband'; }
152 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
156 Class method which returns a qsearch hash expression to search for parameters
157 specified in HASHREF.
163 =item unlinked - set to search for all unlinked services. Overrides all other options.
173 =item pkgpart - arrayref
175 =item routernum - arrayref
177 =item sectornum - arrayref
179 =item towernum - arrayref
188 my( $class, $params, $from, $where ) = @_;
190 #routernum, can be arrayref
191 for my $routernum ( $params->{'routernum'} ) {
192 # this no longer uses addr_block
193 if ( ref $routernum and grep { $_ } @$routernum ) {
194 my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
196 push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
197 push @orwhere, "svc_broadband.routernum IS NULL"
198 if grep /^none$/, @$routernum;
199 push @$where, '( '.join(' OR ', @orwhere).' )';
201 elsif ( $routernum =~ /^(\d+)$/ ) {
202 push @$where, "svc_broadband.routernum = $1";
204 elsif ( $routernum eq 'none' ) {
205 push @$where, "svc_broadband.routernum IS NULL";
209 #this should probably move to svc_Tower_Mixin, or maybe we never should have
210 # done svc_acct # towers (or, as mark thought, never should have done
213 #sector and tower, as above
214 my @where_sector = $class->tower_sector_sql($params);
215 if ( @where_sector ) {
216 push @$where, @where_sector;
217 push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
221 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
222 push @$where, "ip_addr = '$1'";
227 =item search_sql STRING
229 Class method which returns an SQL fragment to search for the given string.
234 my( $class, $string ) = @_;
235 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
236 $class->search_sql_field('ip_addr', $string );
237 } elsif ( $string =~ /^([a-fA-F0-9]{12})$/ ) {
238 $class->search_sql_field('mac_addr', uc($string));
239 } elsif ( $string =~ /^(([a-fA-F0-9]{1,2}:){5}([a-fA-F0-9]{1,2}))$/ ) {
240 $class->search_sql_field('mac_addr', uc("$2$3$4$5$6$7") );
241 } elsif ( $string =~ /^(\d+)$/ ) {
242 my $table = $class->table;
243 "$table.svcnum = $1";
249 =item smart_search STRING
254 my( $class, $string ) = @_;
256 'table' => $class->table, #'svc_broadband',
258 'extra_sql' => 'WHERE '. $class->search_sql($string),
264 Returns the IP address.
270 my $label = 'IP:'. ($self->ip_addr || 'Unknown');
271 $label .= ', MAC:'. $self->mac_addr
273 $label .= ' ('. $self->description. ')'
274 if $self->description;
278 =item insert [ , OPTION => VALUE ... ]
280 Adds this record to the database. If there is an error, returns the error,
281 otherwise returns false.
283 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
284 defined. An FS::cust_svc record will be created and inserted.
286 Currently available options are: I<depend_jobnum>
288 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
289 jobnums), all provisioning jobs will have a dependancy on the supplied
290 jobnum(s) (they will not run until the specific job(s) complete(s)).
294 # Standard FS::svc_Common::insert
298 Delete this record from the database.
302 # Standard FS::svc_Common::delete
304 =item replace OLD_RECORD
306 Replaces the OLD_RECORD with this one in the database. If there is an error,
307 returns the error, otherwise returns false.
309 # Standard FS::svc_Common::replace
313 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
317 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
321 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
325 Checks all fields to make sure this is a valid broadband service. If there is
326 an error, returns the error, otherwise returns false. Called by the insert
333 my $x = $self->setfixed;
335 return $x unless ref($x);
338 my $mac_addr = uc($self->get('mac_addr'));
339 $mac_addr =~ s/[\W_]//g;
340 $self->set('mac_addr', $mac_addr);
343 $self->ut_numbern('svcnum')
344 || $self->ut_numbern('blocknum')
345 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
346 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
347 || $self->ut_textn('description')
348 || $self->ut_numbern('speed_up')
349 || $self->ut_numbern('speed_down')
350 || $self->ut_ipn('ip_addr')
351 || $self->ut_hexn('mac_addr')
352 || $self->ut_hexn('auth_key')
353 || $self->ut_coordn('latitude')
354 || $self->ut_coordn('longitude')
355 || $self->ut_sfloatn('altitude')
356 || $self->ut_textn('vlan_profile')
357 || $self->ut_textn('plan_id')
358 || $self->ut_alphan('radio_serialnum')
359 || $self->ut_textn('radio_location')
360 || $self->ut_textn('poe_location')
361 || $self->ut_snumbern('rssi')
362 || $self->ut_numbern('suid')
363 || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
365 return $error if $error;
367 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
368 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
370 my $cust_svc = $self->svcnum
371 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
376 $cust_pkg = $cust_svc->cust_pkg;
377 $svcpart = $cust_svc->svcpart;
379 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
380 return "Invalid pkgnum" unless $cust_pkg;
381 $svcpart = $self->svcpart;
383 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
385 # assign IP address / router / block
386 $error = $self->svc_ip_check;
387 return $error if $error;
389 and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
390 return 'IP address is required';
393 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
394 my $l = $cust_pkg->cust_location_or_main;
395 if ( $l->ship_latitude && $l->ship_longitude ) {
396 $self->latitude( $l->ship_latitude );
397 $self->longitude( $l->ship_longitude );
398 } elsif ( $l->latitude && $l->longitude ) {
399 $self->latitude( $l->latitude );
400 $self->longitude( $l->longitude );
407 sub _check_duplicate {
409 # Not a reliable check because the table isn't locked, but
410 # that's why we have a unique index. This is just to give a
411 # friendlier error message.
413 @dup = $self->find_duplicates('global', 'mac_addr');
415 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
421 =item mac_addr_formatted CASE DELIMITER
423 Format the MAC address (for use by exports). If CASE starts with "l"
424 (for "lowercase"), it's returned in lowercase. DELIMITER is inserted
429 sub mac_addr_formatted {
431 my ($case, $delim) = @_;
432 my $addr = $self->mac_addr;
433 $addr = lc($addr) if $case =~ /^l/i;
434 join( $delim || '', $addr =~ /../g );
441 local($FS::svc_Common::noexport_hack) = 1;
443 # fix wrong-case MAC addresses
445 $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
448 # set routernum to addr_block.routernum
449 foreach my $self (qsearch('svc_broadband', {
450 blocknum => {op => '!=', value => ''},
453 my $addr_block = $self->addr_block;
454 if ( !$addr_block ) {
455 # super paranoid mode
456 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
459 my $ip_addr = $self->ip_addr;
460 my $routernum = $addr_block->routernum;
462 $self->set(routernum => $routernum);
463 my $error = $self->check;
464 # sanity check: don't allow this to change IP address or block
465 # (other than setting blocknum to null for a non-auto-assigned router)
466 if ( $self->ip_addr ne $ip_addr
467 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
468 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
472 $error ||= $self->replace;
473 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
474 ":\n$error; skipped\n"
478 warn "svcnum ".$self->svcnum.
479 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
483 # assign blocknums to services that should have them
484 my @all_blocks = qsearch('addr_block', { });
485 SVC: foreach my $self (
487 'select' => 'svc_broadband.*',
488 'table' => 'svc_broadband',
489 'addl_from' => 'JOIN router USING (routernum)',
491 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
492 'AND router.manual_addr IS NULL',
496 next SVC if $self->ip_addr eq '';
497 my $NetAddr = $self->NetAddr;
498 # inefficient, but should only need to run once
499 foreach my $block (@all_blocks) {
500 if ($block->NetAddr->contains($NetAddr)) {
501 $self->set(blocknum => $block->blocknum);
502 my $error = $self->replace;
503 warn "WARNING: error assigning blocknum ".$block->blocknum.
504 " to service ".$self->svcnum."\n$error; skipped\n"
509 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
521 The business with sb_field has been 'fixed', in a manner of speaking.
523 allowed_routers isn't agent virtualized because part_svc isn't agent
526 Having both routernum and blocknum as foreign keys is somewhat dubious.
530 FS::svc_Common, FS::Record, FS::addr_block,
531 FS::part_svc, schema.html from the base documentation.