1 package FS::svc_broadband;
13 { no warnings 'redefine'; use NetAddr::IP; }
14 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',
108 'speed_down' => 'Download speed (Kbps)',
109 'speed_up' => 'Upload speed (Kbps)',
110 'ip_addr' => 'IP address',
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,
138 'radio_serialnum' => 'Radio Serial Number',
139 'radio_location' => 'Radio Location',
140 'poe_location' => 'POE Location',
143 'shared_svcnum' => { label => 'Shared Service',
144 type => 'search-svc_broadband',
145 disable_inventory => 1,
151 sub table { 'svc_broadband'; }
153 sub table_dupcheck_fields { ( 'ip_addr', 'mac_addr' ); }
157 Class method which returns a qsearch hash expression to search for parameters
158 specified in HASHREF.
164 =item unlinked - set to search for all unlinked services. Overrides all other options.
174 =item pkgpart - arrayref
176 =item routernum - arrayref
178 =item sectornum - arrayref
180 =item towernum - arrayref
189 my( $class, $params, $from, $where ) = @_;
191 #routernum, can be arrayref
192 for my $routernum ( $params->{'routernum'} ) {
193 # this no longer uses addr_block
194 if ( ref $routernum and grep { $_ } @$routernum ) {
195 my $in = join(',', map { /^(\d+)$/ ? $1 : () } @$routernum );
197 push @orwhere, "svc_broadband.routernum IN ($in)" if $in;
198 push @orwhere, "svc_broadband.routernum IS NULL"
199 if grep /^none$/, @$routernum;
200 push @$where, '( '.join(' OR ', @orwhere).' )';
202 elsif ( $routernum =~ /^(\d+)$/ ) {
203 push @$where, "svc_broadband.routernum = $1";
205 elsif ( $routernum eq 'none' ) {
206 push @$where, "svc_broadband.routernum IS NULL";
210 #this should probably move to svc_Tower_Mixin, or maybe we never should have
211 # done svc_acct # towers (or, as mark thought, never should have done
214 #sector and tower, as above
215 my @where_sector = $class->tower_sector_sql($params);
216 if ( @where_sector ) {
217 push @$where, @where_sector;
218 push @$from, 'LEFT JOIN tower_sector USING ( sectornum )';
222 if ( $params->{'ip_addr'} =~ /^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/ ) {
223 push @$where, "ip_addr = '$1'";
228 =item search_sql STRING
230 Class method which returns an SQL fragment to search for the given string.
235 my( $class, $string ) = @_;
236 if ( $string =~ /^(\d{1,3}\.){3}\d{1,3}$/ ) {
237 $class->search_sql_field('ip_addr', $string );
238 } elsif ( $string =~ /^([A-F0-9]{12})$/i ) {
239 $class->search_sql_field('mac_addr', uc($string));
240 } elsif ( $string =~ /^(([A-F0-9]{2}:){5}([A-F0-9]{2}))$/i ) {
242 $class->search_sql_field('mac_addr', uc($string) );
243 } elsif ( $string =~ /^(\d+)$/ ) {
244 my $table = $class->table;
245 "$table.svcnum = $1";
251 =item smart_search STRING
256 my( $class, $string ) = @_;
258 'table' => $class->table, #'svc_broadband',
260 'extra_sql' => 'WHERE '. $class->search_sql($string),
266 Returns the IP address, MAC address and description.
272 my $label = 'IP:'. ($self->ip_addr || 'Unknown');
273 $label .= ', MAC:'. $self->mac_addr
275 $label .= ' ('. $self->description. ')'
276 if $self->description;
280 =item insert [ , OPTION => VALUE ... ]
282 Adds this record to the database. If there is an error, returns the error,
283 otherwise returns false.
285 The additional fields pkgnum and svcpart (see FS::cust_svc) should be
286 defined. An FS::cust_svc record will be created and inserted.
288 Currently available options are: I<depend_jobnum>
290 If I<depend_jobnum> is set (to a scalar jobnum or an array reference of
291 jobnums), all provisioning jobs will have a dependancy on the supplied
292 jobnum(s) (they will not run until the specific job(s) complete(s)).
296 # Standard FS::svc_Common::insert
300 Delete this record from the database.
304 # Standard FS::svc_Common::delete
306 =item replace OLD_RECORD
308 Replaces the OLD_RECORD with this one in the database. If there is an error,
309 returns the error, otherwise returns false.
311 # Standard FS::svc_Common::replace
315 Called by the suspend method of FS::cust_pkg (see FS::cust_pkg).
319 Called by the unsuspend method of FS::cust_pkg (see FS::cust_pkg).
323 Called by the cancel method of FS::cust_pkg (see FS::cust_pkg).
327 Checks all fields to make sure this is a valid broadband service. If there is
328 an error, returns the error, otherwise returns false. Called by the insert
335 my $x = $self->setfixed;
337 return $x unless ref($x);
340 my $mac_addr = uc($self->get('mac_addr'));
341 $mac_addr =~ s/[\W_]//g;
342 $self->set('mac_addr', $mac_addr);
345 $self->ut_numbern('svcnum')
346 || $self->ut_numbern('blocknum')
347 || $self->ut_foreign_keyn('routernum', 'router', 'routernum')
348 || $self->ut_foreign_keyn('sectornum', 'tower_sector', 'sectornum')
349 || $self->ut_textn('description')
350 || $self->ut_numbern('speed_up')
351 || $self->ut_numbern('speed_down')
352 || $self->ut_ipn('ip_addr')
353 || $self->ut_hexn('mac_addr')
354 || $self->ut_hexn('auth_key')
355 || $self->ut_coordn('latitude')
356 || $self->ut_coordn('longitude')
357 || $self->ut_sfloatn('altitude')
358 || $self->ut_textn('vlan_profile')
359 || $self->ut_textn('plan_id')
360 || $self->ut_alphan('radio_serialnum')
361 || $self->ut_textn('radio_location')
362 || $self->ut_textn('poe_location')
363 || $self->ut_snumbern('rssi')
364 || $self->ut_numbern('suid')
365 || $self->ut_foreign_keyn('shared_svcnum', 'svc_broadband', 'svcnum')
367 return $error if $error;
369 if(($self->speed_up || 0) < 0) { return 'speed_up must be positive'; }
370 if(($self->speed_down || 0) < 0) { return 'speed_down must be positive'; }
372 my $cust_svc = $self->svcnum
373 ? qsearchs('cust_svc', { 'svcnum' => $self->svcnum } )
378 $cust_pkg = $cust_svc->cust_pkg;
379 $svcpart = $cust_svc->svcpart;
381 $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $self->pkgnum } );
382 return "Invalid pkgnum" unless $cust_pkg;
383 $svcpart = $self->svcpart;
385 my $agentnum = $cust_pkg->cust_main->agentnum if $cust_pkg;
387 # assign IP address / router / block
388 $error = $self->svc_ip_check;
389 return $error if $error;
391 and !$conf->exists('svc_broadband-allow_null_ip_addr') ) {
392 return 'IP address is required';
395 if ( $cust_pkg && ! $self->latitude && ! $self->longitude ) {
396 my $l = $cust_pkg->cust_location_or_main;
397 if ( $l->ship_latitude && $l->ship_longitude ) {
398 $self->latitude( $l->ship_latitude );
399 $self->longitude( $l->ship_longitude );
400 } elsif ( $l->latitude && $l->longitude ) {
401 $self->latitude( $l->latitude );
402 $self->longitude( $l->longitude );
409 sub _check_duplicate {
411 # Not a reliable check because the table isn't locked, but
412 # that's why we have a unique index. This is just to give a
413 # friendlier error message.
415 @dup = $self->find_duplicates('global', 'mac_addr');
417 return "MAC address in use (svcnum ".$dup[0]->svcnum.")";
427 local($FS::svc_Common::noexport_hack) = 1;
429 # fix wrong-case MAC addresses
431 $dbh->do('UPDATE svc_broadband SET mac_addr = UPPER(mac_addr);')
434 # set routernum to addr_block.routernum
435 foreach my $self (qsearch('svc_broadband', {
436 blocknum => {op => '!=', value => ''},
439 my $addr_block = $self->addr_block;
440 if ( !$addr_block ) {
441 # super paranoid mode
442 warn "WARNING: svcnum ".$self->svcnum." is assigned to addr_block ".$self->blocknum.", which does not exist; skipped.\n";
445 my $ip_addr = $self->ip_addr;
446 my $routernum = $addr_block->routernum;
448 $self->set(routernum => $routernum);
449 my $error = $self->check;
450 # sanity check: don't allow this to change IP address or block
451 # (other than setting blocknum to null for a non-auto-assigned router)
452 if ( $self->ip_addr ne $ip_addr
453 or ($self->blocknum and $self->blocknum != $addr_block->blocknum)) {
454 warn "WARNING: Upgrading service ".$self->svcnum." would change its block/address; skipped.\n";
458 $error ||= $self->replace;
459 warn "WARNING: error assigning routernum $routernum to service ".$self->svcnum.
460 ":\n$error; skipped\n"
464 warn "svcnum ".$self->svcnum.
465 ": no routernum in address block ".$addr_block->cidr.", skipped\n";
469 # assign blocknums to services that should have them
470 my @all_blocks = qsearch('addr_block', { });
471 SVC: foreach my $self (
473 'select' => 'svc_broadband.*',
474 'table' => 'svc_broadband',
475 'addl_from' => 'JOIN router USING (routernum)',
477 'extra_sql' => 'WHERE svc_broadband.blocknum IS NULL '.
478 'AND router.manual_addr IS NULL',
482 next SVC if $self->ip_addr eq '';
483 my $NetAddr = $self->NetAddr;
484 # inefficient, but should only need to run once
485 foreach my $block (@all_blocks) {
486 if ($block->NetAddr->contains($NetAddr)) {
487 $self->set(blocknum => $block->blocknum);
488 my $error = $self->replace;
489 warn "WARNING: error assigning blocknum ".$block->blocknum.
490 " to service ".$self->svcnum."\n$error; skipped\n"
495 warn "WARNING: no block found containing ".$NetAddr->addr." for service ".
507 The business with sb_field has been 'fixed', in a manner of speaking.
509 allowed_routers isn't agent virtualized because part_svc isn't agent
512 Having both routernum and blocknum as foreign keys is somewhat dubious.
516 FS::svc_Common, FS::Record, FS::addr_block,
517 FS::part_svc, schema.html from the base documentation.